From 1b3a3407cba08ed577dc8878376154311c6fe78d Mon Sep 17 00:00:00 2001 From: Zane Wolfgang Pickett Date: Tue, 18 Nov 2025 21:16:15 -0800 Subject: [PATCH 01/17] Add Anthropic Tools Common Format --- .../providers/anthropic/transforms.rb | 82 +++++ .../test_agent_common_format_input_schema.yml | 209 +++++++++++ ...est_agent_common_format_multiple_tools.yml | 206 +++++++++++ .../test_agent_common_format_parameters.yml | 209 +++++++++++ ...t_agent_common_format_tool_choice_auto.yml | 105 ++++++ ...ent_common_format_tool_choice_required.yml | 207 +++++++++++ ...ent_common_format_tool_choice_specific.yml | 207 +++++++++++ .../anthropic/common_format/tools_test.rb | 347 ++++++++++++++++++ 8 files changed, 1572 insertions(+) create mode 100644 test/fixtures/vcr_cassettes/integration/anthropic/common_format/tools_test/test_agent_common_format_input_schema.yml create mode 100644 test/fixtures/vcr_cassettes/integration/anthropic/common_format/tools_test/test_agent_common_format_multiple_tools.yml create mode 100644 test/fixtures/vcr_cassettes/integration/anthropic/common_format/tools_test/test_agent_common_format_parameters.yml create mode 100644 test/fixtures/vcr_cassettes/integration/anthropic/common_format/tools_test/test_agent_common_format_tool_choice_auto.yml create mode 100644 test/fixtures/vcr_cassettes/integration/anthropic/common_format/tools_test/test_agent_common_format_tool_choice_required.yml create mode 100644 test/fixtures/vcr_cassettes/integration/anthropic/common_format/tools_test/test_agent_common_format_tool_choice_specific.yml create mode 100644 test/integration/anthropic/common_format/tools_test.rb diff --git a/lib/active_agent/providers/anthropic/transforms.rb b/lib/active_agent/providers/anthropic/transforms.rb index e0bccb13..e1c6a736 100644 --- a/lib/active_agent/providers/anthropic/transforms.rb +++ b/lib/active_agent/providers/anthropic/transforms.rb @@ -28,9 +28,90 @@ def normalize_params(params) params = params.dup params[:messages] = normalize_messages(params[:messages]) if params[:messages] params[:system] = normalize_system(params[:system]) if params[:system] + params[:tools] = normalize_tools(params[:tools]) if params[:tools] + params[:tool_choice] = normalize_tool_choice(params[:tool_choice]) if params[:tool_choice] params end + # Normalizes tools from common format to Anthropic format. + # + # Accepts both `parameters` and `input_schema` keys, converting to Anthropic's `input_schema`. + # + # @param tools [Array] + # @return [Array] + def normalize_tools(tools) + return tools unless tools.is_a?(Array) + + tools.map do |tool| + tool_hash = tool.is_a?(Hash) ? tool.deep_symbolize_keys : tool + + # If already in Anthropic format (has input_schema), return as-is + next tool_hash if tool_hash[:input_schema] + + # Convert common format with 'parameters' to Anthropic format with 'input_schema' + if tool_hash[:parameters] + tool_hash = tool_hash.dup + tool_hash[:input_schema] = tool_hash.delete(:parameters) + end + + tool_hash + end + end + + # Normalizes tool_choice from common format to Anthropic gem model objects. + # + # The Anthropic gem expects tool_choice to be a model object (ToolChoiceAuto, + # ToolChoiceAny, ToolChoiceTool, etc.), not a plain hash. + # + # Maps: + # - "required" → ToolChoiceAny (force tool use) + # - "auto" → ToolChoiceAuto (let model decide) + # - { name: "..." } → ToolChoiceTool with name + # + # @param tool_choice [String, Hash, Object] + # @return [Object] Anthropic gem model object + def normalize_tool_choice(tool_choice) + # If already a gem model object, return as-is + return tool_choice if tool_choice.is_a?(::Anthropic::Models::ToolChoiceAuto) || + tool_choice.is_a?(::Anthropic::Models::ToolChoiceAny) || + tool_choice.is_a?(::Anthropic::Models::ToolChoiceTool) || + tool_choice.is_a?(::Anthropic::Models::ToolChoiceNone) + + case tool_choice + when "required" + # Create ToolChoiceAny model for forcing tool use + ::Anthropic::Models::ToolChoiceAny.new(type: :any) + when "auto" + # Create ToolChoiceAuto model for letting model decide + ::Anthropic::Models::ToolChoiceAuto.new(type: :auto) + when Hash + choice_hash = tool_choice.deep_symbolize_keys + + # If has type field, create appropriate model + if choice_hash[:type] + case choice_hash[:type].to_sym + when :any + ::Anthropic::Models::ToolChoiceAny.new(**choice_hash) + when :auto + ::Anthropic::Models::ToolChoiceAuto.new(**choice_hash) + when :tool + ::Anthropic::Models::ToolChoiceTool.new(**choice_hash) + when :none + ::Anthropic::Models::ToolChoiceNone.new(**choice_hash) + else + choice_hash + end + # Convert { name: "..." } to ToolChoiceTool + elsif choice_hash[:name] + ::Anthropic::Models::ToolChoiceTool.new(type: :tool, name: choice_hash[:name]) + else + choice_hash + end + else + tool_choice + end + end + # Merges consecutive same-role messages into single messages with multiple content blocks. # # Required by Anthropic API - consecutive messages with the same role must be combined. @@ -320,6 +401,7 @@ def cleanup_serialized_request(hash, defaults, gem_object = nil) # Remove provider-internal fields that should not be in API request hash.delete(:mcp_servers) # Provider-level config, not API param hash.delete(:stop_sequences) if hash[:stop_sequences] == [] + hash.delete(:tool_choice) if hash[:tool_choice].nil? # Don't send null tool_choice # Remove default values (except max_tokens which is required by API) defaults.each do |key, value| diff --git a/test/fixtures/vcr_cassettes/integration/anthropic/common_format/tools_test/test_agent_common_format_input_schema.yml b/test/fixtures/vcr_cassettes/integration/anthropic/common_format/tools_test/test_agent_common_format_input_schema.yml new file mode 100644 index 00000000..3fc48e1e --- /dev/null +++ b/test/fixtures/vcr_cassettes/integration/anthropic/common_format/tools_test/test_agent_common_format_input_schema.yml @@ -0,0 +1,209 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.anthropic.com/v1/messages + body: + encoding: UTF-8 + string: '{"model":"claude-haiku-4-5","max_tokens":1024,"messages":[{"content":"What''s + the weather in Boston?","role":"user"}],"tools":[{"input_schema":{"type":"object","properties":{"location":{"type":"string","description":"The + city and state, e.g. San Francisco, CA"}},"required":["location"]},"name":"get_weather","description":"Get + the current weather in a given location"}]}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json + User-Agent: + - Anthropic::Client/Ruby 1.14.0 + Host: + - api.anthropic.com + X-Stainless-Arch: + - arm64 + X-Stainless-Lang: + - ruby + X-Stainless-Os: + - MacOS + X-Stainless-Package-Version: + - 1.14.0 + X-Stainless-Runtime: + - ruby + X-Stainless-Runtime-Version: + - 3.4.7 + Content-Type: + - application/json + Anthropic-Version: + - '2023-06-01' + X-Api-Key: + - ACCESS_TOKEN + X-Stainless-Retry-Count: + - '0' + X-Stainless-Timeout: + - '600.0' + Content-Length: + - '370' + response: + status: + code: 200 + message: OK + headers: + Date: + - Wed, 19 Nov 2025 05:13:53 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Anthropic-Ratelimit-Input-Tokens-Limit: + - '4000000' + Anthropic-Ratelimit-Input-Tokens-Remaining: + - '4000000' + Anthropic-Ratelimit-Input-Tokens-Reset: + - '2025-11-19T05:13:53Z' + Anthropic-Ratelimit-Output-Tokens-Limit: + - '800000' + Anthropic-Ratelimit-Output-Tokens-Remaining: + - '800000' + Anthropic-Ratelimit-Output-Tokens-Reset: + - '2025-11-19T05:13:53Z' + Anthropic-Ratelimit-Requests-Limit: + - '4000' + Anthropic-Ratelimit-Requests-Remaining: + - '3999' + Anthropic-Ratelimit-Requests-Reset: + - '2025-11-19T05:13:52Z' + Retry-After: + - '7' + Anthropic-Ratelimit-Tokens-Limit: + - '4800000' + Anthropic-Ratelimit-Tokens-Remaining: + - '4800000' + Anthropic-Ratelimit-Tokens-Reset: + - '2025-11-19T05:13:53Z' + Request-Id: + - req_011CVGgCyKtoGwvJ4LZMLEX1 + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Anthropic-Organization-Id: + - 2557c2f2-bcfa-4054-9fa8-ae13b3b47d6b + X-Envoy-Upstream-Service-Time: + - '1361' + Cf-Cache-Status: + - DYNAMIC + X-Robots-Tag: + - none + Server: + - cloudflare + Cf-Ray: + - 9a0d38849d1c176a-SJC + body: + encoding: ASCII-8BIT + string: '{"model":"claude-haiku-4-5-20251001","id":"msg_01AWL8cLBBTtvG3abMDNTavE","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_014zxU3N9j3gJnV6wKahfpXA","name":"get_weather","input":{"location":"Boston, + MA"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":584,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":56,"service_tier":"standard"}}' + recorded_at: Wed, 19 Nov 2025 05:13:53 GMT +- request: + method: post + uri: https://api.anthropic.com/v1/messages + body: + encoding: UTF-8 + string: '{"model":"claude-haiku-4-5","max_tokens":1024,"messages":[{"content":"What''s + the weather in Boston?","role":"user"},{"content":[{"id":"toolu_014zxU3N9j3gJnV6wKahfpXA","input":{"location":"Boston, + MA"},"name":"get_weather","type":"tool_use"}],"role":"assistant"},{"content":[{"tool_use_id":"toolu_014zxU3N9j3gJnV6wKahfpXA","type":"tool_result","content":"{\"location\":\"Boston, + MA\",\"temperature\":\"72°F\",\"conditions\":\"sunny\"}","is_error":false}],"role":"user"}],"tools":[{"input_schema":{"type":"object","properties":{"location":{"type":"string","description":"The + city and state, e.g. San Francisco, CA"}},"required":["location"]},"name":"get_weather","description":"Get + the current weather in a given location"}]}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json + User-Agent: + - Anthropic::Client/Ruby 1.14.0 + Host: + - api.anthropic.com + X-Stainless-Arch: + - arm64 + X-Stainless-Lang: + - ruby + X-Stainless-Os: + - MacOS + X-Stainless-Package-Version: + - 1.14.0 + X-Stainless-Runtime: + - ruby + X-Stainless-Runtime-Version: + - 3.4.7 + Content-Type: + - application/json + Anthropic-Version: + - '2023-06-01' + X-Api-Key: + - ACCESS_TOKEN + X-Stainless-Retry-Count: + - '0' + X-Stainless-Timeout: + - '600.0' + Content-Length: + - '724' + response: + status: + code: 200 + message: OK + headers: + Date: + - Wed, 19 Nov 2025 05:13:54 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Anthropic-Ratelimit-Input-Tokens-Limit: + - '4000000' + Anthropic-Ratelimit-Input-Tokens-Remaining: + - '4000000' + Anthropic-Ratelimit-Input-Tokens-Reset: + - '2025-11-19T05:13:54Z' + Anthropic-Ratelimit-Output-Tokens-Limit: + - '800000' + Anthropic-Ratelimit-Output-Tokens-Remaining: + - '800000' + Anthropic-Ratelimit-Output-Tokens-Reset: + - '2025-11-19T05:13:54Z' + Anthropic-Ratelimit-Requests-Limit: + - '4000' + Anthropic-Ratelimit-Requests-Remaining: + - '3999' + Anthropic-Ratelimit-Requests-Reset: + - '2025-11-19T05:13:53Z' + Retry-After: + - '6' + Anthropic-Ratelimit-Tokens-Limit: + - '4800000' + Anthropic-Ratelimit-Tokens-Remaining: + - '4800000' + Anthropic-Ratelimit-Tokens-Reset: + - '2025-11-19T05:13:54Z' + Request-Id: + - req_011CVGgD62yK5v6BTMo2zNfb + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Anthropic-Organization-Id: + - 2557c2f2-bcfa-4054-9fa8-ae13b3b47d6b + X-Envoy-Upstream-Service-Time: + - '1093' + Cf-Cache-Status: + - DYNAMIC + X-Robots-Tag: + - none + Server: + - cloudflare + Cf-Ray: + - 9a0d388e7d9d6896-SJC + body: + encoding: ASCII-8BIT + string: !binary |- + eyJtb2RlbCI6ImNsYXVkZS1oYWlrdS00LTUtMjAyNTEwMDEiLCJpZCI6Im1zZ18wMUtybmQycUdGRTFUTHVMNkdVVGhUOWsiLCJ0eXBlIjoibWVzc2FnZSIsInJvbGUiOiJhc3Npc3RhbnQiLCJjb250ZW50IjpbeyJ0eXBlIjoidGV4dCIsInRleHQiOiJUaGUgd2VhdGhlciBpbiBCb3N0b24sIE1BIGlzIGN1cnJlbnRseSAqKnN1bm55Kiogd2l0aCBhIHRlbXBlcmF0dXJlIG9mICoqNzLCsEYqKi4gSXQncyBhIG5pY2UgZGF5ISJ9XSwic3RvcF9yZWFzb24iOiJlbmRfdHVybiIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInVzYWdlIjp7ImlucHV0X3Rva2VucyI6NjY5LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjAsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJjYWNoZV9jcmVhdGlvbiI6eyJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowfSwib3V0cHV0X3Rva2VucyI6MzAsInNlcnZpY2VfdGllciI6InN0YW5kYXJkIn19 + recorded_at: Wed, 19 Nov 2025 05:13:54 GMT +recorded_with: VCR 6.3.1 diff --git a/test/fixtures/vcr_cassettes/integration/anthropic/common_format/tools_test/test_agent_common_format_multiple_tools.yml b/test/fixtures/vcr_cassettes/integration/anthropic/common_format/tools_test/test_agent_common_format_multiple_tools.yml new file mode 100644 index 00000000..d28549a1 --- /dev/null +++ b/test/fixtures/vcr_cassettes/integration/anthropic/common_format/tools_test/test_agent_common_format_multiple_tools.yml @@ -0,0 +1,206 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.anthropic.com/v1/messages + body: + encoding: UTF-8 + string: '{"model":"claude-haiku-4-5","max_tokens":1024,"messages":[{"content":"What''s + the weather in NYC and what''s 5 plus 3?","role":"user"}],"tools":[{"input_schema":{"type":"object","properties":{"location":{"type":"string"}},"required":["location"]},"name":"get_weather","description":"Get + the current weather"},{"input_schema":{"type":"object","properties":{"operation":{"type":"string","enum":["add","subtract","multiply","divide"]},"a":{"type":"number"},"b":{"type":"number"}},"required":["operation","a","b"]},"name":"calculate","description":"Perform + basic arithmetic"}]}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json + User-Agent: + - Anthropic::Client/Ruby 1.14.0 + Host: + - api.anthropic.com + X-Stainless-Arch: + - arm64 + X-Stainless-Lang: + - ruby + X-Stainless-Os: + - MacOS + X-Stainless-Package-Version: + - 1.14.0 + X-Stainless-Runtime: + - ruby + X-Stainless-Runtime-Version: + - 3.4.7 + Content-Type: + - application/json + Anthropic-Version: + - '2023-06-01' + X-Api-Key: + - ACCESS_TOKEN + X-Stainless-Retry-Count: + - '0' + X-Stainless-Timeout: + - '600.0' + Content-Length: + - '571' + response: + status: + code: 200 + message: OK + headers: + Date: + - Wed, 19 Nov 2025 05:13:56 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Anthropic-Ratelimit-Input-Tokens-Limit: + - '4000000' + Anthropic-Ratelimit-Input-Tokens-Remaining: + - '4000000' + Anthropic-Ratelimit-Input-Tokens-Reset: + - '2025-11-19T05:13:55Z' + Anthropic-Ratelimit-Output-Tokens-Limit: + - '800000' + Anthropic-Ratelimit-Output-Tokens-Remaining: + - '800000' + Anthropic-Ratelimit-Output-Tokens-Reset: + - '2025-11-19T05:13:56Z' + Anthropic-Ratelimit-Requests-Limit: + - '4000' + Anthropic-Ratelimit-Requests-Remaining: + - '3999' + Anthropic-Ratelimit-Requests-Reset: + - '2025-11-19T05:13:55Z' + Retry-After: + - '5' + Anthropic-Ratelimit-Tokens-Limit: + - '4800000' + Anthropic-Ratelimit-Tokens-Remaining: + - '4800000' + Anthropic-Ratelimit-Tokens-Reset: + - '2025-11-19T05:13:55Z' + Request-Id: + - req_011CVGgDBV93Cuj7Hpw2cchR + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Anthropic-Organization-Id: + - 2557c2f2-bcfa-4054-9fa8-ae13b3b47d6b + X-Envoy-Upstream-Service-Time: + - '1310' + Cf-Cache-Status: + - DYNAMIC + X-Robots-Tag: + - none + Server: + - cloudflare + Cf-Ray: + - 9a0d38967c3cf957-SJC + body: + encoding: ASCII-8BIT + string: '{"model":"claude-haiku-4-5-20251001","id":"msg_01DjAW69igMMdeAKZ5mzzbyY","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_016rFuCULXZGHkktxmEjaPpJ","name":"get_weather","input":{"location":"NYC"}},{"type":"tool_use","id":"toolu_01RnqNzPw77hCENNf1w5XtMg","name":"calculate","input":{"operation":"add","a":5,"b":3}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":662,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":122,"service_tier":"standard"}}' + recorded_at: Wed, 19 Nov 2025 05:13:56 GMT +- request: + method: post + uri: https://api.anthropic.com/v1/messages + body: + encoding: UTF-8 + string: '{"model":"claude-haiku-4-5","max_tokens":1024,"messages":[{"content":"What''s + the weather in NYC and what''s 5 plus 3?","role":"user"},{"content":[{"id":"toolu_016rFuCULXZGHkktxmEjaPpJ","input":{"location":"NYC"},"name":"get_weather","type":"tool_use"},{"id":"toolu_01RnqNzPw77hCENNf1w5XtMg","input":{"operation":"add","a":5,"b":3},"name":"calculate","type":"tool_use"}],"role":"assistant"},{"content":[{"tool_use_id":"toolu_016rFuCULXZGHkktxmEjaPpJ","type":"tool_result","content":"{\"location\":\"NYC\",\"temperature\":\"72°F\",\"conditions\":\"sunny\"}","is_error":false},{"tool_use_id":"toolu_01RnqNzPw77hCENNf1w5XtMg","type":"tool_result","content":"{\"operation\":\"add\",\"a\":5,\"b\":3,\"result\":8}","is_error":false}],"role":"user"}],"tools":[{"input_schema":{"type":"object","properties":{"location":{"type":"string"}},"required":["location"]},"name":"get_weather","description":"Get + the current weather"},{"input_schema":{"type":"object","properties":{"operation":{"type":"string","enum":["add","subtract","multiply","divide"]},"a":{"type":"number"},"b":{"type":"number"}},"required":["operation","a","b"]},"name":"calculate","description":"Perform + basic arithmetic"}]}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json + User-Agent: + - Anthropic::Client/Ruby 1.14.0 + Host: + - api.anthropic.com + X-Stainless-Arch: + - arm64 + X-Stainless-Lang: + - ruby + X-Stainless-Os: + - MacOS + X-Stainless-Package-Version: + - 1.14.0 + X-Stainless-Runtime: + - ruby + X-Stainless-Runtime-Version: + - 3.4.7 + Content-Type: + - application/json + Anthropic-Version: + - '2023-06-01' + X-Api-Key: + - ACCESS_TOKEN + X-Stainless-Retry-Count: + - '0' + X-Stainless-Timeout: + - '600.0' + Content-Length: + - '1180' + response: + status: + code: 200 + message: OK + headers: + Date: + - Wed, 19 Nov 2025 05:13:57 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Anthropic-Ratelimit-Input-Tokens-Limit: + - '4000000' + Anthropic-Ratelimit-Input-Tokens-Remaining: + - '4000000' + Anthropic-Ratelimit-Input-Tokens-Reset: + - '2025-11-19T05:13:57Z' + Anthropic-Ratelimit-Output-Tokens-Limit: + - '800000' + Anthropic-Ratelimit-Output-Tokens-Remaining: + - '800000' + Anthropic-Ratelimit-Output-Tokens-Reset: + - '2025-11-19T05:13:57Z' + Anthropic-Ratelimit-Requests-Limit: + - '4000' + Anthropic-Ratelimit-Requests-Remaining: + - '3999' + Anthropic-Ratelimit-Requests-Reset: + - '2025-11-19T05:13:56Z' + Retry-After: + - '3' + Anthropic-Ratelimit-Tokens-Limit: + - '4800000' + Anthropic-Ratelimit-Tokens-Remaining: + - '4800000' + Anthropic-Ratelimit-Tokens-Reset: + - '2025-11-19T05:13:57Z' + Request-Id: + - req_011CVGgDHgwtCewKNMwnASAR + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Anthropic-Organization-Id: + - 2557c2f2-bcfa-4054-9fa8-ae13b3b47d6b + X-Envoy-Upstream-Service-Time: + - '1304' + Cf-Cache-Status: + - DYNAMIC + X-Robots-Tag: + - none + Server: + - cloudflare + Cf-Ray: + - 9a0d389f8cc9f95f-SJC + body: + encoding: ASCII-8BIT + string: !binary |- + eyJtb2RlbCI6ImNsYXVkZS1oYWlrdS00LTUtMjAyNTEwMDEiLCJpZCI6Im1zZ18wMU1KNmRvdHlmTW9iSmZwV01XcEp2MVEiLCJ0eXBlIjoibWVzc2FnZSIsInJvbGUiOiJhc3Npc3RhbnQiLCJjb250ZW50IjpbeyJ0eXBlIjoidGV4dCIsInRleHQiOiJHcmVhdCEgSGVyZSdzIHRoZSBpbmZvcm1hdGlvbiB5b3UgcmVxdWVzdGVkOlxuXG4qKldlYXRoZXIgaW4gTllDOioqIEl0J3MgY3VycmVudGx5IDcywrBGIGFuZCBzdW5ueSEg4piA77iPXG5cbioqNSBwbHVzIDM6KiogVGhlIGFuc3dlciBpcyAqKjgqKiJ9XSwic3RvcF9yZWFzb24iOiJlbmRfdHVybiIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInVzYWdlIjp7ImlucHV0X3Rva2VucyI6ODc2LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjAsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJjYWNoZV9jcmVhdGlvbiI6eyJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowfSwib3V0cHV0X3Rva2VucyI6NDYsInNlcnZpY2VfdGllciI6InN0YW5kYXJkIn19 + recorded_at: Wed, 19 Nov 2025 05:13:57 GMT +recorded_with: VCR 6.3.1 diff --git a/test/fixtures/vcr_cassettes/integration/anthropic/common_format/tools_test/test_agent_common_format_parameters.yml b/test/fixtures/vcr_cassettes/integration/anthropic/common_format/tools_test/test_agent_common_format_parameters.yml new file mode 100644 index 00000000..d6e6a103 --- /dev/null +++ b/test/fixtures/vcr_cassettes/integration/anthropic/common_format/tools_test/test_agent_common_format_parameters.yml @@ -0,0 +1,209 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.anthropic.com/v1/messages + body: + encoding: UTF-8 + string: '{"model":"claude-haiku-4-5","max_tokens":1024,"messages":[{"content":"What''s + the weather in San Francisco?","role":"user"}],"tools":[{"input_schema":{"type":"object","properties":{"location":{"type":"string","description":"The + city and state, e.g. San Francisco, CA"}},"required":["location"]},"name":"get_weather","description":"Get + the current weather in a given location"}]}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json + User-Agent: + - Anthropic::Client/Ruby 1.14.0 + Host: + - api.anthropic.com + X-Stainless-Arch: + - arm64 + X-Stainless-Lang: + - ruby + X-Stainless-Os: + - MacOS + X-Stainless-Package-Version: + - 1.14.0 + X-Stainless-Runtime: + - ruby + X-Stainless-Runtime-Version: + - 3.4.7 + Content-Type: + - application/json + Anthropic-Version: + - '2023-06-01' + X-Api-Key: + - ACCESS_TOKEN + X-Stainless-Retry-Count: + - '0' + X-Stainless-Timeout: + - '600.0' + Content-Length: + - '377' + response: + status: + code: 200 + message: OK + headers: + Date: + - Wed, 19 Nov 2025 05:13:45 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Anthropic-Ratelimit-Input-Tokens-Limit: + - '4000000' + Anthropic-Ratelimit-Input-Tokens-Remaining: + - '4000000' + Anthropic-Ratelimit-Input-Tokens-Reset: + - '2025-11-19T05:13:45Z' + Anthropic-Ratelimit-Output-Tokens-Limit: + - '800000' + Anthropic-Ratelimit-Output-Tokens-Remaining: + - '800000' + Anthropic-Ratelimit-Output-Tokens-Reset: + - '2025-11-19T05:13:45Z' + Anthropic-Ratelimit-Requests-Limit: + - '4000' + Anthropic-Ratelimit-Requests-Remaining: + - '3999' + Anthropic-Ratelimit-Requests-Reset: + - '2025-11-19T05:13:44Z' + Retry-After: + - '15' + Anthropic-Ratelimit-Tokens-Limit: + - '4800000' + Anthropic-Ratelimit-Tokens-Remaining: + - '4800000' + Anthropic-Ratelimit-Tokens-Reset: + - '2025-11-19T05:13:45Z' + Request-Id: + - req_011CVGgCPE3eGZd6g9EB3PE3 + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Anthropic-Organization-Id: + - 2557c2f2-bcfa-4054-9fa8-ae13b3b47d6b + X-Envoy-Upstream-Service-Time: + - '1084' + Cf-Cache-Status: + - DYNAMIC + X-Robots-Tag: + - none + Server: + - cloudflare + Cf-Ray: + - 9a0d3852ce872517-SJC + body: + encoding: ASCII-8BIT + string: '{"model":"claude-haiku-4-5-20251001","id":"msg_01EuuY4DyAnyry5fLgdVzk1w","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01UXUH8LCk3paEsKxjqLayG2","name":"get_weather","input":{"location":"San + Francisco, CA"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":585,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":57,"service_tier":"standard"}}' + recorded_at: Wed, 19 Nov 2025 05:13:45 GMT +- request: + method: post + uri: https://api.anthropic.com/v1/messages + body: + encoding: UTF-8 + string: '{"model":"claude-haiku-4-5","max_tokens":1024,"messages":[{"content":"What''s + the weather in San Francisco?","role":"user"},{"content":[{"id":"toolu_01UXUH8LCk3paEsKxjqLayG2","input":{"location":"San + Francisco, CA"},"name":"get_weather","type":"tool_use"}],"role":"assistant"},{"content":[{"tool_use_id":"toolu_01UXUH8LCk3paEsKxjqLayG2","type":"tool_result","content":"{\"location\":\"San + Francisco, CA\",\"temperature\":\"72°F\",\"conditions\":\"sunny\"}","is_error":false}],"role":"user"}],"tools":[{"input_schema":{"type":"object","properties":{"location":{"type":"string","description":"The + city and state, e.g. San Francisco, CA"}},"required":["location"]},"name":"get_weather","description":"Get + the current weather in a given location"}]}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json + User-Agent: + - Anthropic::Client/Ruby 1.14.0 + Host: + - api.anthropic.com + X-Stainless-Arch: + - arm64 + X-Stainless-Lang: + - ruby + X-Stainless-Os: + - MacOS + X-Stainless-Package-Version: + - 1.14.0 + X-Stainless-Runtime: + - ruby + X-Stainless-Runtime-Version: + - 3.4.7 + Content-Type: + - application/json + Anthropic-Version: + - '2023-06-01' + X-Api-Key: + - ACCESS_TOKEN + X-Stainless-Retry-Count: + - '0' + X-Stainless-Timeout: + - '600.0' + Content-Length: + - '745' + response: + status: + code: 200 + message: OK + headers: + Date: + - Wed, 19 Nov 2025 05:13:46 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Anthropic-Ratelimit-Input-Tokens-Limit: + - '4000000' + Anthropic-Ratelimit-Input-Tokens-Remaining: + - '4000000' + Anthropic-Ratelimit-Input-Tokens-Reset: + - '2025-11-19T05:13:46Z' + Anthropic-Ratelimit-Output-Tokens-Limit: + - '800000' + Anthropic-Ratelimit-Output-Tokens-Remaining: + - '800000' + Anthropic-Ratelimit-Output-Tokens-Reset: + - '2025-11-19T05:13:46Z' + Anthropic-Ratelimit-Requests-Limit: + - '4000' + Anthropic-Ratelimit-Requests-Remaining: + - '3999' + Anthropic-Ratelimit-Requests-Reset: + - '2025-11-19T05:13:45Z' + Retry-After: + - '14' + Anthropic-Ratelimit-Tokens-Limit: + - '4800000' + Anthropic-Ratelimit-Tokens-Remaining: + - '4800000' + Anthropic-Ratelimit-Tokens-Reset: + - '2025-11-19T05:13:46Z' + Request-Id: + - req_011CVGgCUUohvAoZzNCtpVEA + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Anthropic-Organization-Id: + - 2557c2f2-bcfa-4054-9fa8-ae13b3b47d6b + X-Envoy-Upstream-Service-Time: + - '929' + Cf-Cache-Status: + - DYNAMIC + X-Robots-Tag: + - none + Server: + - cloudflare + Cf-Ray: + - 9a0d385a7a78d025-SJC + body: + encoding: ASCII-8BIT + string: !binary |- + eyJtb2RlbCI6ImNsYXVkZS1oYWlrdS00LTUtMjAyNTEwMDEiLCJpZCI6Im1zZ18wMU5HdmlKZmpDZUxwRHVKN1NKMXZZdFkiLCJ0eXBlIjoibWVzc2FnZSIsInJvbGUiOiJhc3Npc3RhbnQiLCJjb250ZW50IjpbeyJ0eXBlIjoidGV4dCIsInRleHQiOiJUaGUgd2VhdGhlciBpbiBTYW4gRnJhbmNpc2NvLCBDQSBpcyBjdXJyZW50bHkgKipzdW5ueSoqIHdpdGggYSB0ZW1wZXJhdHVyZSBvZiAqKjcywrBGKiouIEl0J3MgYSBiZWF1dGlmdWwgZGF5ISJ9XSwic3RvcF9yZWFzb24iOiJlbmRfdHVybiIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInVzYWdlIjp7ImlucHV0X3Rva2VucyI6NjcyLCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjAsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJjYWNoZV9jcmVhdGlvbiI6eyJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowfSwib3V0cHV0X3Rva2VucyI6MzEsInNlcnZpY2VfdGllciI6InN0YW5kYXJkIn19 + recorded_at: Wed, 19 Nov 2025 05:13:46 GMT +recorded_with: VCR 6.3.1 diff --git a/test/fixtures/vcr_cassettes/integration/anthropic/common_format/tools_test/test_agent_common_format_tool_choice_auto.yml b/test/fixtures/vcr_cassettes/integration/anthropic/common_format/tools_test/test_agent_common_format_tool_choice_auto.yml new file mode 100644 index 00000000..e45b13f2 --- /dev/null +++ b/test/fixtures/vcr_cassettes/integration/anthropic/common_format/tools_test/test_agent_common_format_tool_choice_auto.yml @@ -0,0 +1,105 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.anthropic.com/v1/messages + body: + encoding: UTF-8 + string: '{"model":"claude-haiku-4-5","max_tokens":1024,"messages":[{"content":"What''s + the weather?","role":"user"}],"tools":[{"input_schema":{"type":"object","properties":{"location":{"type":"string"}},"required":["location"]},"name":"get_weather","description":"Get + weather"}],"tool_choice":{"type":"auto"}}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json + User-Agent: + - Anthropic::Client/Ruby 1.14.0 + Host: + - api.anthropic.com + X-Stainless-Arch: + - arm64 + X-Stainless-Lang: + - ruby + X-Stainless-Os: + - MacOS + X-Stainless-Package-Version: + - 1.14.0 + X-Stainless-Runtime: + - ruby + X-Stainless-Runtime-Version: + - 3.4.7 + Content-Type: + - application/json + Anthropic-Version: + - '2023-06-01' + X-Api-Key: + - ACCESS_TOKEN + X-Stainless-Retry-Count: + - '0' + X-Stainless-Timeout: + - '600.0' + Content-Length: + - '299' + response: + status: + code: 200 + message: OK + headers: + Date: + - Wed, 19 Nov 2025 05:13:59 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Anthropic-Ratelimit-Input-Tokens-Limit: + - '4000000' + Anthropic-Ratelimit-Input-Tokens-Remaining: + - '4000000' + Anthropic-Ratelimit-Input-Tokens-Reset: + - '2025-11-19T05:13:58Z' + Anthropic-Ratelimit-Output-Tokens-Limit: + - '800000' + Anthropic-Ratelimit-Output-Tokens-Remaining: + - '800000' + Anthropic-Ratelimit-Output-Tokens-Reset: + - '2025-11-19T05:13:59Z' + Anthropic-Ratelimit-Requests-Limit: + - '4000' + Anthropic-Ratelimit-Requests-Remaining: + - '3999' + Anthropic-Ratelimit-Requests-Reset: + - '2025-11-19T05:13:57Z' + Retry-After: + - '2' + Anthropic-Ratelimit-Tokens-Limit: + - '4800000' + Anthropic-Ratelimit-Tokens-Remaining: + - '4800000' + Anthropic-Ratelimit-Tokens-Reset: + - '2025-11-19T05:13:58Z' + Request-Id: + - req_011CVGgDPzxgDVtcZxRcL3VR + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Anthropic-Organization-Id: + - 2557c2f2-bcfa-4054-9fa8-ae13b3b47d6b + X-Envoy-Upstream-Service-Time: + - '1146' + Cf-Cache-Status: + - DYNAMIC + X-Robots-Tag: + - none + Server: + - cloudflare + Cf-Ray: + - 9a0d38a8bff5f9f5-SJC + body: + encoding: ASCII-8BIT + string: '{"model":"claude-haiku-4-5-20251001","id":"msg_012dzMywWG636J19CAyU6R1z","type":"message","role":"assistant","content":[{"type":"text","text":"I''d + be happy to help you with the weather! However, I need to know your location. + Could you please tell me which city or location you''d like the weather for?"}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":558,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":39,"service_tier":"standard"}}' + recorded_at: Wed, 19 Nov 2025 05:13:59 GMT +recorded_with: VCR 6.3.1 diff --git a/test/fixtures/vcr_cassettes/integration/anthropic/common_format/tools_test/test_agent_common_format_tool_choice_required.yml b/test/fixtures/vcr_cassettes/integration/anthropic/common_format/tools_test/test_agent_common_format_tool_choice_required.yml new file mode 100644 index 00000000..0fb3aebe --- /dev/null +++ b/test/fixtures/vcr_cassettes/integration/anthropic/common_format/tools_test/test_agent_common_format_tool_choice_required.yml @@ -0,0 +1,207 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.anthropic.com/v1/messages + body: + encoding: UTF-8 + string: '{"model":"claude-haiku-4-5","max_tokens":1024,"messages":[{"content":"What''s + the weather?","role":"user"}],"tools":[{"input_schema":{"type":"object","properties":{"location":{"type":"string"}},"required":["location"]},"name":"get_weather","description":"Get + weather"}],"tool_choice":{"type":"any"}}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json + User-Agent: + - Anthropic::Client/Ruby 1.14.0 + Host: + - api.anthropic.com + X-Stainless-Arch: + - arm64 + X-Stainless-Lang: + - ruby + X-Stainless-Os: + - MacOS + X-Stainless-Package-Version: + - 1.14.0 + X-Stainless-Runtime: + - ruby + X-Stainless-Runtime-Version: + - 3.4.7 + Content-Type: + - application/json + Anthropic-Version: + - '2023-06-01' + X-Api-Key: + - ACCESS_TOKEN + X-Stainless-Retry-Count: + - '0' + X-Stainless-Timeout: + - '600.0' + Content-Length: + - '298' + response: + status: + code: 200 + message: OK + headers: + Date: + - Wed, 19 Nov 2025 05:13:47 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Anthropic-Ratelimit-Input-Tokens-Limit: + - '4000000' + Anthropic-Ratelimit-Input-Tokens-Remaining: + - '4000000' + Anthropic-Ratelimit-Input-Tokens-Reset: + - '2025-11-19T05:13:47Z' + Anthropic-Ratelimit-Output-Tokens-Limit: + - '800000' + Anthropic-Ratelimit-Output-Tokens-Remaining: + - '800000' + Anthropic-Ratelimit-Output-Tokens-Reset: + - '2025-11-19T05:13:47Z' + Anthropic-Ratelimit-Requests-Limit: + - '4000' + Anthropic-Ratelimit-Requests-Remaining: + - '3999' + Anthropic-Ratelimit-Requests-Reset: + - '2025-11-19T05:13:46Z' + Retry-After: + - '13' + Anthropic-Ratelimit-Tokens-Limit: + - '4800000' + Anthropic-Ratelimit-Tokens-Remaining: + - '4800000' + Anthropic-Ratelimit-Tokens-Reset: + - '2025-11-19T05:13:47Z' + Request-Id: + - req_011CVGgCZL1VuQAp6qtQtH4E + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Anthropic-Organization-Id: + - 2557c2f2-bcfa-4054-9fa8-ae13b3b47d6b + X-Envoy-Upstream-Service-Time: + - '1053' + Cf-Cache-Status: + - DYNAMIC + X-Robots-Tag: + - none + Server: + - cloudflare + Cf-Ray: + - 9a0d38619d7e5c1f-SJC + body: + encoding: ASCII-8BIT + string: '{"model":"claude-haiku-4-5-20251001","id":"msg_01GrmnfB317zDPsKT3smbXAw","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_018gB1gszzTHn8ajnaihUDVH","name":"get_weather","input":{"location":"current + location"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":650,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":39,"service_tier":"standard"}}' + recorded_at: Wed, 19 Nov 2025 05:13:47 GMT +- request: + method: post + uri: https://api.anthropic.com/v1/messages + body: + encoding: UTF-8 + string: '{"model":"claude-haiku-4-5","max_tokens":1024,"messages":[{"content":"What''s + the weather?","role":"user"},{"content":[{"id":"toolu_018gB1gszzTHn8ajnaihUDVH","input":{"location":"current + location"},"name":"get_weather","type":"tool_use"}],"role":"assistant"},{"content":[{"tool_use_id":"toolu_018gB1gszzTHn8ajnaihUDVH","type":"tool_result","content":"{\"location\":\"current + location\",\"temperature\":\"72°F\",\"conditions\":\"sunny\"}","is_error":false}],"role":"user"}],"tools":[{"input_schema":{"type":"object","properties":{"location":{"type":"string"}},"required":["location"]},"name":"get_weather","description":"Get + weather"}]}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json + User-Agent: + - Anthropic::Client/Ruby 1.14.0 + Host: + - api.anthropic.com + X-Stainless-Arch: + - arm64 + X-Stainless-Lang: + - ruby + X-Stainless-Os: + - MacOS + X-Stainless-Package-Version: + - 1.14.0 + X-Stainless-Runtime: + - ruby + X-Stainless-Runtime-Version: + - 3.4.7 + Content-Type: + - application/json + Anthropic-Version: + - '2023-06-01' + X-Api-Key: + - ACCESS_TOKEN + X-Stainless-Retry-Count: + - '0' + X-Stainless-Timeout: + - '600.0' + Content-Length: + - '635' + response: + status: + code: 200 + message: OK + headers: + Date: + - Wed, 19 Nov 2025 05:13:48 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Anthropic-Ratelimit-Input-Tokens-Limit: + - '4000000' + Anthropic-Ratelimit-Input-Tokens-Remaining: + - '4000000' + Anthropic-Ratelimit-Input-Tokens-Reset: + - '2025-11-19T05:13:48Z' + Anthropic-Ratelimit-Output-Tokens-Limit: + - '800000' + Anthropic-Ratelimit-Output-Tokens-Remaining: + - '800000' + Anthropic-Ratelimit-Output-Tokens-Reset: + - '2025-11-19T05:13:48Z' + Anthropic-Ratelimit-Requests-Limit: + - '4000' + Anthropic-Ratelimit-Requests-Remaining: + - '3999' + Anthropic-Ratelimit-Requests-Reset: + - '2025-11-19T05:13:47Z' + Retry-After: + - '12' + Anthropic-Ratelimit-Tokens-Limit: + - '4800000' + Anthropic-Ratelimit-Tokens-Remaining: + - '4800000' + Anthropic-Ratelimit-Tokens-Reset: + - '2025-11-19T05:13:48Z' + Request-Id: + - req_011CVGgCeejuG18Mghs594NC + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Anthropic-Organization-Id: + - 2557c2f2-bcfa-4054-9fa8-ae13b3b47d6b + X-Envoy-Upstream-Service-Time: + - '1120' + Cf-Cache-Status: + - DYNAMIC + X-Robots-Tag: + - none + Server: + - cloudflare + Cf-Ray: + - 9a0d38695f81fac6-SJC + body: + encoding: ASCII-8BIT + string: !binary |- + eyJtb2RlbCI6ImNsYXVkZS1oYWlrdS00LTUtMjAyNTEwMDEiLCJpZCI6Im1zZ18wMVlRaWNTTktxa3FESjQxYlNGelB4U2IiLCJ0eXBlIjoibWVzc2FnZSIsInJvbGUiOiJhc3Npc3RhbnQiLCJjb250ZW50IjpbeyJ0eXBlIjoidGV4dCIsInRleHQiOiJUaGUgd2VhdGhlciBhdCB5b3VyIGN1cnJlbnQgbG9jYXRpb24gaXM6XG4tICoqVGVtcGVyYXR1cmUqKjogNzLCsEZcbi0gKipDb25kaXRpb25zKio6IFN1bm55XG5cbkl0J3MgYSBuaWNlIHN1bm55IGRheSEgSXMgdGhlcmUgYW55dGhpbmcgZWxzZSB5b3UnZCBsaWtlIHRvIGtub3cgYWJvdXQgdGhlIHdlYXRoZXI/In1dLCJzdG9wX3JlYXNvbiI6ImVuZF90dXJuIiwic3RvcF9zZXF1ZW5jZSI6bnVsbCwidXNhZ2UiOnsiaW5wdXRfdG9rZW5zIjo2NDEsImNhY2hlX2NyZWF0aW9uX2lucHV0X3Rva2VucyI6MCwiY2FjaGVfcmVhZF9pbnB1dF90b2tlbnMiOjAsImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjB9LCJvdXRwdXRfdG9rZW5zIjo0OSwic2VydmljZV90aWVyIjoic3RhbmRhcmQifX0= + recorded_at: Wed, 19 Nov 2025 05:13:48 GMT +recorded_with: VCR 6.3.1 diff --git a/test/fixtures/vcr_cassettes/integration/anthropic/common_format/tools_test/test_agent_common_format_tool_choice_specific.yml b/test/fixtures/vcr_cassettes/integration/anthropic/common_format/tools_test/test_agent_common_format_tool_choice_specific.yml new file mode 100644 index 00000000..6d73430f --- /dev/null +++ b/test/fixtures/vcr_cassettes/integration/anthropic/common_format/tools_test/test_agent_common_format_tool_choice_specific.yml @@ -0,0 +1,207 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.anthropic.com/v1/messages + body: + encoding: UTF-8 + string: '{"model":"claude-haiku-4-5","max_tokens":1024,"messages":[{"content":"What''s + the weather?","role":"user"}],"tools":[{"input_schema":{"type":"object","properties":{"location":{"type":"string"}},"required":["location"]},"name":"get_weather","description":"Get + weather"}],"tool_choice":{"type":"tool","name":"get_weather"}}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json + User-Agent: + - Anthropic::Client/Ruby 1.14.0 + Host: + - api.anthropic.com + X-Stainless-Arch: + - arm64 + X-Stainless-Lang: + - ruby + X-Stainless-Os: + - MacOS + X-Stainless-Package-Version: + - 1.14.0 + X-Stainless-Runtime: + - ruby + X-Stainless-Runtime-Version: + - 3.4.7 + Content-Type: + - application/json + Anthropic-Version: + - '2023-06-01' + X-Api-Key: + - ACCESS_TOKEN + X-Stainless-Retry-Count: + - '0' + X-Stainless-Timeout: + - '600.0' + Content-Length: + - '320' + response: + status: + code: 200 + message: OK + headers: + Date: + - Wed, 19 Nov 2025 05:13:49 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Anthropic-Ratelimit-Input-Tokens-Limit: + - '4000000' + Anthropic-Ratelimit-Input-Tokens-Remaining: + - '4000000' + Anthropic-Ratelimit-Input-Tokens-Reset: + - '2025-11-19T05:13:49Z' + Anthropic-Ratelimit-Output-Tokens-Limit: + - '800000' + Anthropic-Ratelimit-Output-Tokens-Remaining: + - '800000' + Anthropic-Ratelimit-Output-Tokens-Reset: + - '2025-11-19T05:13:49Z' + Anthropic-Ratelimit-Requests-Limit: + - '4000' + Anthropic-Ratelimit-Requests-Remaining: + - '3999' + Anthropic-Ratelimit-Requests-Reset: + - '2025-11-19T05:13:49Z' + Retry-After: + - '11' + Anthropic-Ratelimit-Tokens-Limit: + - '4800000' + Anthropic-Ratelimit-Tokens-Remaining: + - '4800000' + Anthropic-Ratelimit-Tokens-Reset: + - '2025-11-19T05:13:49Z' + Request-Id: + - req_011CVGgCk7QdbL9Ro4qS1mDn + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Anthropic-Organization-Id: + - 2557c2f2-bcfa-4054-9fa8-ae13b3b47d6b + X-Envoy-Upstream-Service-Time: + - '885' + Cf-Cache-Status: + - DYNAMIC + X-Robots-Tag: + - none + Server: + - cloudflare + Cf-Ray: + - 9a0d38715cbaf555-SJC + body: + encoding: ASCII-8BIT + string: '{"model":"claude-haiku-4-5-20251001","id":"msg_01NviDBZtqZfGTS5vAD8Vo6z","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01RnUd1Xv78Dx6XPssao9Eou","name":"get_weather","input":{"location":"current + location"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":655,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":34,"service_tier":"standard"}}' + recorded_at: Wed, 19 Nov 2025 05:13:49 GMT +- request: + method: post + uri: https://api.anthropic.com/v1/messages + body: + encoding: UTF-8 + string: '{"model":"claude-haiku-4-5","max_tokens":1024,"messages":[{"content":"What''s + the weather?","role":"user"},{"content":[{"id":"toolu_01RnUd1Xv78Dx6XPssao9Eou","input":{"location":"current + location"},"name":"get_weather","type":"tool_use"}],"role":"assistant"},{"content":[{"tool_use_id":"toolu_01RnUd1Xv78Dx6XPssao9Eou","type":"tool_result","content":"{\"location\":\"current + location\",\"temperature\":\"72°F\",\"conditions\":\"sunny\"}","is_error":false}],"role":"user"}],"tools":[{"input_schema":{"type":"object","properties":{"location":{"type":"string"}},"required":["location"]},"name":"get_weather","description":"Get + weather"}]}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json + User-Agent: + - Anthropic::Client/Ruby 1.14.0 + Host: + - api.anthropic.com + X-Stainless-Arch: + - arm64 + X-Stainless-Lang: + - ruby + X-Stainless-Os: + - MacOS + X-Stainless-Package-Version: + - 1.14.0 + X-Stainless-Runtime: + - ruby + X-Stainless-Runtime-Version: + - 3.4.7 + Content-Type: + - application/json + Anthropic-Version: + - '2023-06-01' + X-Api-Key: + - ACCESS_TOKEN + X-Stainless-Retry-Count: + - '0' + X-Stainless-Timeout: + - '600.0' + Content-Length: + - '635' + response: + status: + code: 200 + message: OK + headers: + Date: + - Wed, 19 Nov 2025 05:13:52 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Anthropic-Ratelimit-Input-Tokens-Limit: + - '4000000' + Anthropic-Ratelimit-Input-Tokens-Remaining: + - '4000000' + Anthropic-Ratelimit-Input-Tokens-Reset: + - '2025-11-19T05:13:51Z' + Anthropic-Ratelimit-Output-Tokens-Limit: + - '800000' + Anthropic-Ratelimit-Output-Tokens-Remaining: + - '800000' + Anthropic-Ratelimit-Output-Tokens-Reset: + - '2025-11-19T05:13:51Z' + Anthropic-Ratelimit-Requests-Limit: + - '4000' + Anthropic-Ratelimit-Requests-Remaining: + - '3999' + Anthropic-Ratelimit-Requests-Reset: + - '2025-11-19T05:13:50Z' + Retry-After: + - '9' + Anthropic-Ratelimit-Tokens-Limit: + - '4800000' + Anthropic-Ratelimit-Tokens-Remaining: + - '4800000' + Anthropic-Ratelimit-Tokens-Reset: + - '2025-11-19T05:13:51Z' + Request-Id: + - req_011CVGgCpc2RFuCa64fftcsD + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Anthropic-Organization-Id: + - 2557c2f2-bcfa-4054-9fa8-ae13b3b47d6b + X-Envoy-Upstream-Service-Time: + - '1887' + Cf-Cache-Status: + - DYNAMIC + X-Robots-Tag: + - none + Server: + - cloudflare + Cf-Ray: + - 9a0d3877ce7f2832-SJC + body: + encoding: ASCII-8BIT + string: !binary |- + eyJtb2RlbCI6ImNsYXVkZS1oYWlrdS00LTUtMjAyNTEwMDEiLCJpZCI6Im1zZ18wMVdMRHN6cUtoaG1IaFFEREEzbXJIU1QiLCJ0eXBlIjoibWVzc2FnZSIsInJvbGUiOiJhc3Npc3RhbnQiLCJjb250ZW50IjpbeyJ0eXBlIjoidGV4dCIsInRleHQiOiJUaGUgd2VhdGhlciBhdCB5b3VyIGN1cnJlbnQgbG9jYXRpb24gaXMgKipzdW5ueSoqIHdpdGggYSB0ZW1wZXJhdHVyZSBvZiAqKjcywrBGKiouIExvb2tzIGxpa2UgYSBuaWNlIGRheSEgXG5cbklmIHlvdSdkIGxpa2Ugd2VhdGhlciBpbmZvcm1hdGlvbiBmb3IgYSBzcGVjaWZpYyBsb2NhdGlvbiwganVzdCBsZXQgbWUga25vdyB0aGUgY2l0eSBvciBhcmVhIG5hbWUgYW5kIEkgY2FuIGdldCB0aGF0IGZvciB5b3UuIn1dLCJzdG9wX3JlYXNvbiI6ImVuZF90dXJuIiwic3RvcF9zZXF1ZW5jZSI6bnVsbCwidXNhZ2UiOnsiaW5wdXRfdG9rZW5zIjo2NDEsImNhY2hlX2NyZWF0aW9uX2lucHV0X3Rva2VucyI6MCwiY2FjaGVfcmVhZF9pbnB1dF90b2tlbnMiOjAsImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjB9LCJvdXRwdXRfdG9rZW5zIjo2MCwic2VydmljZV90aWVyIjoic3RhbmRhcmQifX0= + recorded_at: Wed, 19 Nov 2025 05:13:52 GMT +recorded_with: VCR 6.3.1 diff --git a/test/integration/anthropic/common_format/tools_test.rb b/test/integration/anthropic/common_format/tools_test.rb new file mode 100644 index 00000000..f6de1da0 --- /dev/null +++ b/test/integration/anthropic/common_format/tools_test.rb @@ -0,0 +1,347 @@ +# frozen_string_literal: true + +require_relative "../../test_helper" + +module Integration + module Anthropic + module CommonFormat + class ToolsTest < ActiveSupport::TestCase + include Integration::TestHelper + + class TestAgent < ActiveAgent::Base + generate_with :anthropic, model: "claude-haiku-4-5", max_tokens: 1024 + + def get_weather(location:) + { location: location, temperature: "72°F", conditions: "sunny" } + end + + def calculate(operation:, a:, b:) + result = case operation + when "add" then a + b + when "subtract" then a - b + when "multiply" then a * b + when "divide" then a / b + end + { operation: operation, a: a, b: b, result: result } + end + + # Common format with 'parameters' key (recommended) + COMMON_FORMAT_PARAMETERS = { + model: "claude-haiku-4-5", + messages: [ + { + role: "user", + content: "What's the weather in San Francisco?" + } + ], + max_tokens: 1024, + tools: [ + { + name: "get_weather", + description: "Get the current weather in a given location", + input_schema: { + type: "object", + properties: { + location: { + type: "string", + description: "The city and state, e.g. San Francisco, CA" + } + }, + required: [ "location" ] + } + } + ] + } + def common_format_parameters + prompt( + message: "What's the weather in San Francisco?", + tools: [ + { + name: "get_weather", + description: "Get the current weather in a given location", + parameters: { + type: "object", + properties: { + location: { + type: "string", + description: "The city and state, e.g. San Francisco, CA" + } + }, + required: [ "location" ] + } + } + ] + ) + end + + # Common format with 'input_schema' key (Anthropic native) + COMMON_FORMAT_INPUT_SCHEMA = { + model: "claude-haiku-4-5", + messages: [ + { + role: "user", + content: "What's the weather in Boston?" + } + ], + max_tokens: 1024, + tools: [ + { + name: "get_weather", + description: "Get the current weather in a given location", + input_schema: { + type: "object", + properties: { + location: { + type: "string", + description: "The city and state, e.g. San Francisco, CA" + } + }, + required: [ "location" ] + } + } + ] + } + def common_format_input_schema + prompt( + message: "What's the weather in Boston?", + tools: [ + { + name: "get_weather", + description: "Get the current weather in a given location", + input_schema: { + type: "object", + properties: { + location: { + type: "string", + description: "The city and state, e.g. San Francisco, CA" + } + }, + required: [ "location" ] + } + } + ] + ) + end + + # Multiple tools in common format + COMMON_FORMAT_MULTIPLE_TOOLS = { + model: "claude-haiku-4-5", + messages: [ + { + role: "user", + content: "What's the weather in NYC and what's 5 plus 3?" + } + ], + max_tokens: 1024, + tools: [ + { + name: "get_weather", + description: "Get the current weather", + input_schema: { + type: "object", + properties: { + location: { type: "string" } + }, + required: [ "location" ] + } + }, + { + name: "calculate", + description: "Perform basic arithmetic", + input_schema: { + type: "object", + properties: { + operation: { type: "string", enum: [ "add", "subtract", "multiply", "divide" ] }, + a: { type: "number" }, + b: { type: "number" } + }, + required: [ "operation", "a", "b" ] + } + } + ] + } + def common_format_multiple_tools + prompt( + message: "What's the weather in NYC and what's 5 plus 3?", + tools: [ + { + name: "get_weather", + description: "Get the current weather", + parameters: { + type: "object", + properties: { + location: { type: "string" } + }, + required: [ "location" ] + } + }, + { + name: "calculate", + description: "Perform basic arithmetic", + parameters: { + type: "object", + properties: { + operation: { type: "string", enum: [ "add", "subtract", "multiply", "divide" ] }, + a: { type: "number" }, + b: { type: "number" } + }, + required: [ "operation", "a", "b" ] + } + } + ] + ) + end + + # Tool choice - string format + COMMON_FORMAT_TOOL_CHOICE_AUTO = { + model: "claude-haiku-4-5", + messages: [ + { + role: "user", + content: "What's the weather?" + } + ], + max_tokens: 1024, + tools: [ + { + name: "get_weather", + description: "Get weather", + input_schema: { + type: "object", + properties: { + location: { type: "string" } + }, + required: [ "location" ] + } + } + ], + tool_choice: { type: "auto" } + } + def common_format_tool_choice_auto + prompt( + message: "What's the weather?", + tools: [ + { + name: "get_weather", + description: "Get weather", + parameters: { + type: "object", + properties: { + location: { type: "string" } + }, + required: [ "location" ] + } + } + ], + tool_choice: "auto" + ) + end + + # Tool choice - force tool use with "required" + COMMON_FORMAT_TOOL_CHOICE_REQUIRED = { + model: "claude-haiku-4-5", + messages: [ + { + role: "user", + content: "What's the weather?" + } + ], + max_tokens: 1024, + tools: [ + { + name: "get_weather", + description: "Get weather", + input_schema: { + type: "object", + properties: { + location: { type: "string" } + }, + required: [ "location" ] + } + } + ], + tool_choice: { type: "any" } + } + def common_format_tool_choice_required + prompt( + message: "What's the weather?", + tools: [ + { + name: "get_weather", + description: "Get weather", + parameters: { + type: "object", + properties: { + location: { type: "string" } + }, + required: [ "location" ] + } + } + ], + tool_choice: "required" + ) + end + + # Tool choice - specific tool + COMMON_FORMAT_TOOL_CHOICE_SPECIFIC = { + model: "claude-haiku-4-5", + messages: [ + { + role: "user", + content: "What's the weather?" + } + ], + max_tokens: 1024, + tools: [ + { + name: "get_weather", + description: "Get weather", + input_schema: { + type: "object", + properties: { + location: { type: "string" } + }, + required: [ "location" ] + } + } + ], + tool_choice: { type: "tool", name: "get_weather" } + } + def common_format_tool_choice_specific + prompt( + message: "What's the weather?", + tools: [ + { + name: "get_weather", + description: "Get weather", + parameters: { + type: "object", + properties: { + location: { type: "string" } + }, + required: [ "location" ] + } + } + ], + tool_choice: { name: "get_weather" } + ) + end + end + + ################################################################################ + # This automatically runs all the tests for the test actions + ################################################################################ + [ + :common_format_parameters, + :common_format_input_schema, + :common_format_multiple_tools, + :common_format_tool_choice_auto, + :common_format_tool_choice_required, + :common_format_tool_choice_specific + ].each do |action_name| + test_request_builder(TestAgent, action_name, :generate_now, TestAgent.const_get(action_name.to_s.upcase, true)) + end + end + end + end +end From b2d377d69b0995521c43d3fc8e77b3b5107c94b3 Mon Sep 17 00:00:00 2001 From: Zane Wolfgang Pickett Date: Tue, 18 Nov 2025 22:12:32 -0800 Subject: [PATCH 02/17] Add OpenAI Response Tools Common Format --- .../providers/open_ai/responses/request.rb | 6 +- .../providers/open_ai/responses/transforms.rb | 99 ++++ .../providers/open_ai/responses_provider.rb | 34 ++ .../test_agent_common_format_input_schema.yml | 379 +++++++++++++++ ...est_agent_common_format_multiple_tools.yml | 449 ++++++++++++++++++ .../test_agent_common_format_parameters.yml | 379 +++++++++++++++ ...t_agent_common_format_tool_choice_auto.yml | 191 ++++++++ ...ent_common_format_tool_choice_required.yml | 375 +++++++++++++++ ...ent_common_format_tool_choice_specific.yml | 378 +++++++++++++++ .../responses/common_format/tools_test.rb | 329 +++++++++++++ 10 files changed, 2618 insertions(+), 1 deletion(-) create mode 100644 test/fixtures/vcr_cassettes/integration/open_ai/responses/common_format/tools_test/test_agent_common_format_input_schema.yml create mode 100644 test/fixtures/vcr_cassettes/integration/open_ai/responses/common_format/tools_test/test_agent_common_format_multiple_tools.yml create mode 100644 test/fixtures/vcr_cassettes/integration/open_ai/responses/common_format/tools_test/test_agent_common_format_parameters.yml create mode 100644 test/fixtures/vcr_cassettes/integration/open_ai/responses/common_format/tools_test/test_agent_common_format_tool_choice_auto.yml create mode 100644 test/fixtures/vcr_cassettes/integration/open_ai/responses/common_format/tools_test/test_agent_common_format_tool_choice_required.yml create mode 100644 test/fixtures/vcr_cassettes/integration/open_ai/responses/common_format/tools_test/test_agent_common_format_tool_choice_specific.yml create mode 100644 test/integration/open_ai/responses/common_format/tools_test.rb diff --git a/lib/active_agent/providers/open_ai/responses/request.rb b/lib/active_agent/providers/open_ai/responses/request.rb index 93d169c4..f42785b0 100644 --- a/lib/active_agent/providers/open_ai/responses/request.rb +++ b/lib/active_agent/providers/open_ai/responses/request.rb @@ -73,7 +73,11 @@ def initialize(**params) # Step 6: Normalize input content for gem compatibility params[:input] = Responses::Transforms.normalize_input(params[:input]) if params[:input] - # Step 7: Create gem model - delegates to OpenAI gem + # Step 7: Normalize tools and tool_choice from common format + params[:tools] = Responses::Transforms.normalize_tools(params[:tools]) if params[:tools] + params[:tool_choice] = Responses::Transforms.normalize_tool_choice(params[:tool_choice]) if params[:tool_choice] + + # Step 8: Create gem model - delegates to OpenAI gem gem_model = ::OpenAI::Models::Responses::ResponseCreateParams.new(**params) # Step 8: Delegate all method calls to gem model diff --git a/lib/active_agent/providers/open_ai/responses/transforms.rb b/lib/active_agent/providers/open_ai/responses/transforms.rb index bfaebbfc..71a5874b 100644 --- a/lib/active_agent/providers/open_ai/responses/transforms.rb +++ b/lib/active_agent/providers/open_ai/responses/transforms.rb @@ -21,6 +21,105 @@ def gem_to_hash(gem_object) JSON.parse(gem_object.to_json, symbolize_names: true) end + # Normalizes tools from common format to OpenAI Responses API format. + # + # Accepts tools in multiple formats: + # - Common format: `{name: "...", description: "...", parameters: {...}}` + # - Nested format: `{type: "function", function: {name: "...", ...}}` + # - Responses format: `{type: "function", name: "...", parameters: {...}}` + # + # Always outputs flat Responses API format. + # + # @param tools [Array] + # @return [Array] + def normalize_tools(tools) + return tools unless tools.is_a?(Array) + + tools.map do |tool| + tool_hash = tool.is_a?(Hash) ? tool.deep_symbolize_keys : tool + + # If already in Responses format (flat with type, name, parameters), return as-is + if tool_hash[:type] == "function" && tool_hash[:name] + next tool_hash + end + + # If in nested Chat API format, flatten it + if tool_hash[:type] == "function" && tool_hash[:function] + func = tool_hash[:function] + next { + type: "function", + name: func[:name], + description: func[:description], + parameters: func[:parameters] || func[:input_schema] + }.compact + end + + # If in common format (no type field), convert to Responses format + if tool_hash[:name] && !tool_hash[:type] + next { + type: "function", + name: tool_hash[:name], + description: tool_hash[:description], + parameters: tool_hash[:parameters] || tool_hash[:input_schema] + }.compact + end + + # Pass through other formats + tool_hash + end + end + + # Normalizes tool_choice from common format to OpenAI Responses API format. + # + # Responses API uses flat format for specific tool choice, unlike Chat API's nested format. + # Must return gem model objects for proper serialization. + # + # Maps: + # - "required" → :required symbol (force tool use) + # - "auto" → :auto symbol (let model decide) + # - { name: "..." } → ToolChoiceFunction model object + # + # @param tool_choice [String, Hash, Object] + # @return [Symbol, Object] Symbol or gem model object + def normalize_tool_choice(tool_choice) + # If already a gem model object, return as-is + return tool_choice if tool_choice.is_a?(::OpenAI::Models::Responses::ToolChoiceFunction) || + tool_choice.is_a?(::OpenAI::Models::Responses::ToolChoiceAllowed) || + tool_choice.is_a?(::OpenAI::Models::Responses::ToolChoiceTypes) || + tool_choice.is_a?(::OpenAI::Models::Responses::ToolChoiceMcp) || + tool_choice.is_a?(::OpenAI::Models::Responses::ToolChoiceCustom) + + case tool_choice + when "required" + :required # Return as symbol + when "auto" + :auto # Return as symbol + when "none" + :none # Return as symbol + when Hash + choice_hash = tool_choice.deep_symbolize_keys + + # If already in proper format with type, try to create gem model + if choice_hash[:type] == "function" && choice_hash[:name] + # Create ToolChoiceFunction gem model object + ::OpenAI::Models::Responses::ToolChoiceFunction.new( + type: :function, + name: choice_hash[:name] + ) + # Convert { name: "..." } to ToolChoiceFunction model + elsif choice_hash[:name] && !choice_hash[:type] + ::OpenAI::Models::Responses::ToolChoiceFunction.new( + type: :function, + name: choice_hash[:name] + ) + else + choice_hash + end + else + tool_choice + end + end + # Simplifies input for cleaner API requests # # Unwraps single-element arrays: diff --git a/lib/active_agent/providers/open_ai/responses_provider.rb b/lib/active_agent/providers/open_ai/responses_provider.rb index b1b498e6..ad164a22 100644 --- a/lib/active_agent/providers/open_ai/responses_provider.rb +++ b/lib/active_agent/providers/open_ai/responses_provider.rb @@ -25,6 +25,40 @@ def self.prompt_request_type protected + # @see BaseProvider#prepare_prompt_request + # @return [Request] + def prepare_prompt_request + prepare_prompt_request_tools + + super + end + + # @api private + def prepare_prompt_request_tools + return unless request.tool_choice + + # Get list of function calls that have been made + # In Responses API, message_stack items are flat - each item has a type field + functions_used = message_stack + .select { |item| item[:type] == "function_call" } + .map { |item| item[:name] } + .compact + + # Check if tool_choice is a gem model object or symbol + if request.tool_choice.is_a?(::OpenAI::Models::Responses::ToolChoiceFunction) + # Specific tool choice - clear if that tool was used + tool_choice_name = request.tool_choice.name + if tool_choice_name && functions_used.include?(tool_choice_name) + request.tool_choice = nil + end + elsif request.tool_choice == :required + # Required tool choice - clear if any tool was used + if functions_used.any? + request.tool_choice = nil + end + end + end + # @return [Object] OpenAI client's responses endpoint def api_prompt_executer client.responses diff --git a/test/fixtures/vcr_cassettes/integration/open_ai/responses/common_format/tools_test/test_agent_common_format_input_schema.yml b/test/fixtures/vcr_cassettes/integration/open_ai/responses/common_format/tools_test/test_agent_common_format_input_schema.yml new file mode 100644 index 00000000..fc4398ae --- /dev/null +++ b/test/fixtures/vcr_cassettes/integration/open_ai/responses/common_format/tools_test/test_agent_common_format_input_schema.yml @@ -0,0 +1,379 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.openai.com/v1/responses + body: + encoding: UTF-8 + string: '{"model":"gpt-4.1","input":"What''s the weather in Boston?","tools":[{"type":"function","name":"get_weather","description":"Get + the current weather in a given location","parameters":{"type":"object","properties":{"location":{"type":"string","description":"The + city and state, e.g. San Francisco, CA"}},"required":["location"]}}],"tool_choice":"auto"}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json + User-Agent: + - OpenAI::Client/Ruby 0.35.1 + Host: + - api.openai.com + X-Stainless-Arch: + - arm64 + X-Stainless-Lang: + - ruby + X-Stainless-Os: + - MacOS + X-Stainless-Package-Version: + - 0.35.1 + X-Stainless-Runtime: + - ruby + X-Stainless-Runtime-Version: + - 3.4.7 + Content-Type: + - application/json + Openai-Organization: + - ORGANIZATION_ID + Openai-Project: + - PROJECT_ID + Authorization: + - Bearer ACCESS_TOKEN + X-Stainless-Retry-Count: + - '0' + X-Stainless-Timeout: + - '600.0' + Content-Length: + - '349' + response: + status: + code: 200 + message: OK + headers: + Date: + - Wed, 19 Nov 2025 06:11:07 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + X-Ratelimit-Limit-Requests: + - '10000' + X-Ratelimit-Limit-Tokens: + - '30000000' + X-Ratelimit-Remaining-Requests: + - '9999' + X-Ratelimit-Remaining-Tokens: + - '29999723' + X-Ratelimit-Reset-Requests: + - 6ms + X-Ratelimit-Reset-Tokens: + - 0s + Openai-Version: + - '2020-10-01' + Openai-Organization: + - ORGANIZATION_ID + Openai-Project: + - PROJECT_ID + X-Request-Id: + - req_a7ec86589c0e452aae830f8e1aab0b9e + Openai-Processing-Ms: + - '698' + X-Envoy-Upstream-Service-Time: + - '700' + Cf-Cache-Status: + - DYNAMIC + Set-Cookie: + - __cf_bm=onoCz1cVaAO3fnxszHT1oDXSaXnTwqJ4vIaaMiK0TNc-1763532667-1.0.1.1-Ab5x7CUkOe4srMhphaKJChvsDvVV5KMkMy6dE143HRbeOTZq_s4V84fSUeeqk_1uiXjbzs4aOTJZ51rqEu_QFm4M2ktvpu0bEexQpCA8JVA; + path=/; expires=Wed, 19-Nov-25 06:41:07 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=2FKxYBOeWG2LIOAqiGHlrDuK2m2YEfALiNHEm_tHMzU-1763532667941-0.0.1.1-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + X-Content-Type-Options: + - nosniff + Server: + - cloudflare + Cf-Ray: + - 9a0d8c618b89cee9-SJC + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: |- + { + "id": "resp_018e356c588e624800691d5f7b36d481968c8ecb0314784e2b", + "object": "response", + "created_at": 1763532667, + "status": "completed", + "background": false, + "billing": { + "payer": "developer" + }, + "error": null, + "incomplete_details": null, + "instructions": null, + "max_output_tokens": null, + "max_tool_calls": null, + "model": "gpt-4.1-2025-04-14", + "output": [ + { + "id": "fc_018e356c588e624800691d5f7baa188196bcc8c4954a75ecc2", + "type": "function_call", + "status": "completed", + "arguments": "{\"location\":\"Boston, MA\"}", + "call_id": "call_FxpdpMpXNm8kcvvVkUwU779y", + "name": "get_weather" + } + ], + "parallel_tool_calls": true, + "previous_response_id": null, + "prompt_cache_key": null, + "prompt_cache_retention": null, + "reasoning": { + "effort": null, + "summary": null + }, + "safety_identifier": null, + "service_tier": "default", + "store": true, + "temperature": 1.0, + "text": { + "format": { + "type": "text" + }, + "verbosity": "medium" + }, + "tool_choice": "auto", + "tools": [ + { + "type": "function", + "description": "Get the current weather in a given location", + "name": "get_weather", + "parameters": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "The city and state, e.g. San Francisco, CA" + } + }, + "required": [ + "location" + ], + "additionalProperties": false + }, + "strict": true + } + ], + "top_logprobs": 0, + "top_p": 1.0, + "truncation": "disabled", + "usage": { + "input_tokens": 60, + "input_tokens_details": { + "cached_tokens": 0 + }, + "output_tokens": 17, + "output_tokens_details": { + "reasoning_tokens": 0 + }, + "total_tokens": 77 + }, + "user": null, + "metadata": {} + } + recorded_at: Wed, 19 Nov 2025 06:11:07 GMT +- request: + method: post + uri: https://api.openai.com/v1/responses + body: + encoding: UTF-8 + string: '{"model":"gpt-4.1","input":[{"role":"user","content":"What''s the weather + in Boston?"},{"arguments":"{\"location\":\"Boston, MA\"}","call_id":"call_FxpdpMpXNm8kcvvVkUwU779y","name":"get_weather","type":"function_call","id":"fc_018e356c588e624800691d5f7baa188196bcc8c4954a75ecc2","status":"completed"},{"call_id":"call_FxpdpMpXNm8kcvvVkUwU779y","output":"{\"location\":\"Boston, + MA\",\"temperature\":\"72°F\",\"conditions\":\"sunny\"}","type":"function_call_output"}],"tools":[{"type":"function","name":"get_weather","description":"Get + the current weather in a given location","parameters":{"type":"object","properties":{"location":{"type":"string","description":"The + city and state, e.g. San Francisco, CA"}},"required":["location"]}}],"tool_choice":"auto"}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json + User-Agent: + - OpenAI::Client/Ruby 0.35.1 + Host: + - api.openai.com + X-Stainless-Arch: + - arm64 + X-Stainless-Lang: + - ruby + X-Stainless-Os: + - MacOS + X-Stainless-Package-Version: + - 0.35.1 + X-Stainless-Runtime: + - ruby + X-Stainless-Runtime-Version: + - 3.4.7 + Content-Type: + - application/json + Openai-Organization: + - ORGANIZATION_ID + Openai-Project: + - PROJECT_ID + Authorization: + - Bearer ACCESS_TOKEN + X-Stainless-Retry-Count: + - '0' + X-Stainless-Timeout: + - '600.0' + Content-Length: + - '757' + response: + status: + code: 200 + message: OK + headers: + Date: + - Wed, 19 Nov 2025 06:11:08 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + X-Ratelimit-Limit-Requests: + - '10000' + X-Ratelimit-Limit-Tokens: + - '30000000' + X-Ratelimit-Remaining-Requests: + - '9999' + X-Ratelimit-Remaining-Tokens: + - '29999681' + X-Ratelimit-Reset-Requests: + - 6ms + X-Ratelimit-Reset-Tokens: + - 0s + Openai-Version: + - '2020-10-01' + Openai-Organization: + - ORGANIZATION_ID + Openai-Project: + - PROJECT_ID + X-Request-Id: + - req_6383c911b15a47e99ce15f9d87da3e54 + Openai-Processing-Ms: + - '869' + X-Envoy-Upstream-Service-Time: + - '872' + Cf-Cache-Status: + - DYNAMIC + Set-Cookie: + - __cf_bm=Dmkk7Dmj3.ATH2iKiVR_5oZDAp9sswe0WCH.LgUW4mw-1763532668-1.0.1.1-fnyEjDID8fVSA3o3Ehh1azinNzYA3QvNaCkZrATSPlesFoYcR6RjOVHWnGBaCqfak1i2_pJumumz8K1HuhGSwlLL3zY4P4kOVXeKO9rI0vE; + path=/; expires=Wed, 19-Nov-25 06:41:08 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=Si4zaXfWhtfuqV6w3ccccjMgU0wS.rabg3lg5ny5eC0-1763532668887-0.0.1.1-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + X-Content-Type-Options: + - nosniff + Server: + - cloudflare + Cf-Ray: + - 9a0d8c66fb7ba666-SJC + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: |- + { + "id": "resp_018e356c588e624800691d5f7c064481968781661d40264bad", + "object": "response", + "created_at": 1763532668, + "status": "completed", + "background": false, + "billing": { + "payer": "developer" + }, + "error": null, + "incomplete_details": null, + "instructions": null, + "max_output_tokens": null, + "max_tool_calls": null, + "model": "gpt-4.1-2025-04-14", + "output": [ + { + "id": "msg_018e356c588e624800691d5f7c7b688196843f1b9f209240f9", + "type": "message", + "status": "completed", + "content": [ + { + "type": "output_text", + "annotations": [], + "logprobs": [], + "text": "The weather in Boston is currently 72\u00b0F and sunny." + } + ], + "role": "assistant" + } + ], + "parallel_tool_calls": true, + "previous_response_id": null, + "prompt_cache_key": null, + "prompt_cache_retention": null, + "reasoning": { + "effort": null, + "summary": null + }, + "safety_identifier": null, + "service_tier": "default", + "store": true, + "temperature": 1.0, + "text": { + "format": { + "type": "text" + }, + "verbosity": "medium" + }, + "tool_choice": "auto", + "tools": [ + { + "type": "function", + "description": "Get the current weather in a given location", + "name": "get_weather", + "parameters": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "The city and state, e.g. San Francisco, CA" + } + }, + "required": [ + "location" + ], + "additionalProperties": false + }, + "strict": true + } + ], + "top_logprobs": 0, + "top_p": 1.0, + "truncation": "disabled", + "usage": { + "input_tokens": 102, + "input_tokens_details": { + "cached_tokens": 0 + }, + "output_tokens": 14, + "output_tokens_details": { + "reasoning_tokens": 0 + }, + "total_tokens": 116 + }, + "user": null, + "metadata": {} + } + recorded_at: Wed, 19 Nov 2025 06:11:08 GMT +recorded_with: VCR 6.3.1 diff --git a/test/fixtures/vcr_cassettes/integration/open_ai/responses/common_format/tools_test/test_agent_common_format_multiple_tools.yml b/test/fixtures/vcr_cassettes/integration/open_ai/responses/common_format/tools_test/test_agent_common_format_multiple_tools.yml new file mode 100644 index 00000000..e87dbed4 --- /dev/null +++ b/test/fixtures/vcr_cassettes/integration/open_ai/responses/common_format/tools_test/test_agent_common_format_multiple_tools.yml @@ -0,0 +1,449 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.openai.com/v1/responses + body: + encoding: UTF-8 + string: '{"model":"gpt-4.1","input":"What''s the weather in NYC and what''s + 5 plus 3?","tools":[{"type":"function","name":"get_weather","description":"Get + the current weather","parameters":{"type":"object","properties":{"location":{"type":"string"}},"required":["location"]}},{"type":"function","name":"calculate","description":"Perform + basic arithmetic","parameters":{"type":"object","properties":{"operation":{"type":"string","enum":["add","subtract","multiply","divide"]},"a":{"type":"number"},"b":{"type":"number"}},"required":["operation","a","b"]}}],"tool_choice":"auto"}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json + User-Agent: + - OpenAI::Client/Ruby 0.35.1 + Host: + - api.openai.com + X-Stainless-Arch: + - arm64 + X-Stainless-Lang: + - ruby + X-Stainless-Os: + - MacOS + X-Stainless-Package-Version: + - 0.35.1 + X-Stainless-Runtime: + - ruby + X-Stainless-Runtime-Version: + - 3.4.7 + Content-Type: + - application/json + Openai-Organization: + - ORGANIZATION_ID + Openai-Project: + - PROJECT_ID + Authorization: + - Bearer ACCESS_TOKEN + X-Stainless-Retry-Count: + - '0' + X-Stainless-Timeout: + - '600.0' + Content-Length: + - '566' + response: + status: + code: 200 + message: OK + headers: + Date: + - Wed, 19 Nov 2025 06:11:10 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + X-Ratelimit-Limit-Requests: + - '10000' + X-Ratelimit-Limit-Tokens: + - '30000000' + X-Ratelimit-Remaining-Requests: + - '9999' + X-Ratelimit-Remaining-Tokens: + - '29999696' + X-Ratelimit-Reset-Requests: + - 6ms + X-Ratelimit-Reset-Tokens: + - 0s + Openai-Version: + - '2020-10-01' + Openai-Organization: + - ORGANIZATION_ID + Openai-Project: + - PROJECT_ID + X-Request-Id: + - req_6918799381d74405b9a62accc69378c0 + Openai-Processing-Ms: + - '1228' + X-Envoy-Upstream-Service-Time: + - '1232' + Cf-Cache-Status: + - DYNAMIC + Set-Cookie: + - __cf_bm=Mnm6UBy1Ew.ZHVCsaJiQLrAQE3OEq5uL42PupnzidB4-1763532670-1.0.1.1-yIcJJBC9OH746vItV.a1guk5oC31jlVRwVuogkaoMUauvg_KWaOk9AxEcxfXxfedwxbrIIFZdNS3cLUSTMu7hIFP6wQRMfpaGr6.gggw15g; + path=/; expires=Wed, 19-Nov-25 06:41:10 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=hG.QaWB4Jd06L3DQqmxyF4YG11P88FguXAu94vxWxIw-1763532670210-0.0.1.1-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + X-Content-Type-Options: + - nosniff + Server: + - cloudflare + Cf-Ray: + - 9a0d8c6cf97eeb22-SJC + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: |- + { + "id": "resp_04e95d07f1de59b200691d5f7cff84819a869de18fad9a448a", + "object": "response", + "created_at": 1763532669, + "status": "completed", + "background": false, + "billing": { + "payer": "developer" + }, + "error": null, + "incomplete_details": null, + "instructions": null, + "max_output_tokens": null, + "max_tool_calls": null, + "model": "gpt-4.1-2025-04-14", + "output": [ + { + "id": "fc_04e95d07f1de59b200691d5f7dd024819a8d138035f4846a34", + "type": "function_call", + "status": "completed", + "arguments": "{\"location\":\"NYC\"}", + "call_id": "call_Zi12SRBwXTIyjZaBdmxfZvLt", + "name": "get_weather" + }, + { + "id": "fc_04e95d07f1de59b200691d5f7dfba4819a996acd9bbf98e8b5", + "type": "function_call", + "status": "completed", + "arguments": "{\"operation\":\"add\",\"a\":5,\"b\":3}", + "call_id": "call_0vSIbaSaYkamroz4tIupYYe0", + "name": "calculate" + } + ], + "parallel_tool_calls": true, + "previous_response_id": null, + "prompt_cache_key": null, + "prompt_cache_retention": null, + "reasoning": { + "effort": null, + "summary": null + }, + "safety_identifier": null, + "service_tier": "default", + "store": true, + "temperature": 1.0, + "text": { + "format": { + "type": "text" + }, + "verbosity": "medium" + }, + "tool_choice": "auto", + "tools": [ + { + "type": "function", + "description": "Get the current weather", + "name": "get_weather", + "parameters": { + "type": "object", + "properties": { + "location": { + "type": "string" + } + }, + "required": [ + "location" + ], + "additionalProperties": false + }, + "strict": true + }, + { + "type": "function", + "description": "Perform basic arithmetic", + "name": "calculate", + "parameters": { + "type": "object", + "properties": { + "operation": { + "type": "string", + "enum": [ + "add", + "subtract", + "multiply", + "divide" + ] + }, + "a": { + "type": "number" + }, + "b": { + "type": "number" + } + }, + "required": [ + "operation", + "a", + "b" + ], + "additionalProperties": false + }, + "strict": true + } + ], + "top_logprobs": 0, + "top_p": 1.0, + "truncation": "disabled", + "usage": { + "input_tokens": 43, + "input_tokens_details": { + "cached_tokens": 0 + }, + "output_tokens": 52, + "output_tokens_details": { + "reasoning_tokens": 0 + }, + "total_tokens": 95 + }, + "user": null, + "metadata": {} + } + recorded_at: Wed, 19 Nov 2025 06:11:10 GMT +- request: + method: post + uri: https://api.openai.com/v1/responses + body: + encoding: UTF-8 + string: '{"model":"gpt-4.1","input":[{"role":"user","content":"What''s the weather + in NYC and what''s 5 plus 3?"},{"arguments":"{\"location\":\"NYC\"}","call_id":"call_Zi12SRBwXTIyjZaBdmxfZvLt","name":"get_weather","type":"function_call","id":"fc_04e95d07f1de59b200691d5f7dd024819a8d138035f4846a34","status":"completed"},{"arguments":"{\"operation\":\"add\",\"a\":5,\"b\":3}","call_id":"call_0vSIbaSaYkamroz4tIupYYe0","name":"calculate","type":"function_call","id":"fc_04e95d07f1de59b200691d5f7dfba4819a996acd9bbf98e8b5","status":"completed"},{"call_id":"call_Zi12SRBwXTIyjZaBdmxfZvLt","output":"{\"location\":\"NYC\",\"temperature\":\"72°F\",\"conditions\":\"sunny\"}","type":"function_call_output"},{"call_id":"call_0vSIbaSaYkamroz4tIupYYe0","output":"{\"operation\":\"add\",\"a\":5,\"b\":3,\"result\":8}","type":"function_call_output"}],"tools":[{"type":"function","name":"get_weather","description":"Get + the current weather","parameters":{"type":"object","properties":{"location":{"type":"string"}},"required":["location"]}},{"type":"function","name":"calculate","description":"Perform + basic arithmetic","parameters":{"type":"object","properties":{"operation":{"type":"string","enum":["add","subtract","multiply","divide"]},"a":{"type":"number"},"b":{"type":"number"}},"required":["operation","a","b"]}}],"tool_choice":"auto"}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json + User-Agent: + - OpenAI::Client/Ruby 0.35.1 + Host: + - api.openai.com + X-Stainless-Arch: + - arm64 + X-Stainless-Lang: + - ruby + X-Stainless-Os: + - MacOS + X-Stainless-Package-Version: + - 0.35.1 + X-Stainless-Runtime: + - ruby + X-Stainless-Runtime-Version: + - 3.4.7 + Content-Type: + - application/json + Openai-Organization: + - ORGANIZATION_ID + Openai-Project: + - PROJECT_ID + Authorization: + - Bearer ACCESS_TOKEN + X-Stainless-Retry-Count: + - '0' + X-Stainless-Timeout: + - '600.0' + Content-Length: + - '1320' + response: + status: + code: 200 + message: OK + headers: + Date: + - Wed, 19 Nov 2025 06:11:11 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + X-Ratelimit-Limit-Requests: + - '10000' + X-Ratelimit-Limit-Tokens: + - '30000000' + X-Ratelimit-Remaining-Requests: + - '9999' + X-Ratelimit-Remaining-Tokens: + - '29999616' + X-Ratelimit-Reset-Requests: + - 6ms + X-Ratelimit-Reset-Tokens: + - 0s + Openai-Version: + - '2020-10-01' + Openai-Organization: + - ORGANIZATION_ID + Openai-Project: + - PROJECT_ID + X-Request-Id: + - req_4bbd5f42683a46dbbb9fa068156c45e6 + Openai-Processing-Ms: + - '955' + X-Envoy-Upstream-Service-Time: + - '957' + Cf-Cache-Status: + - DYNAMIC + Set-Cookie: + - __cf_bm=NGa0hm5RXfB_5LMKYtTFpI9_GfXn60SBMH6Kki9ol3o-1763532671-1.0.1.1-NkjOxHaqZceIE.bG_7kVJ2xvCS7x7c3voCWY1gwUWLgdkwFjwAsJLQ1lmaeLoN7lAR22JXCQR6SuekF.KWVh4vITD5_jXWrvrqFHMZj7H.8; + path=/; expires=Wed, 19-Nov-25 06:41:11 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=djqhwD6mZ7kgb9Ey7SMGATG74aowOLANeEIKcX.Luu0-1763532671334-0.0.1.1-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + X-Content-Type-Options: + - nosniff + Server: + - cloudflare + Cf-Ray: + - 9a0d8c751e066896-SJC + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: |- + { + "id": "resp_04e95d07f1de59b200691d5f7e5bc4819abd93a0c80931ff2b", + "object": "response", + "created_at": 1763532670, + "status": "completed", + "background": false, + "billing": { + "payer": "developer" + }, + "error": null, + "incomplete_details": null, + "instructions": null, + "max_output_tokens": null, + "max_tool_calls": null, + "model": "gpt-4.1-2025-04-14", + "output": [ + { + "id": "msg_04e95d07f1de59b200691d5f7eb608819aa23cefc267f6fb32", + "type": "message", + "status": "completed", + "content": [ + { + "type": "output_text", + "annotations": [], + "logprobs": [], + "text": "The weather in NYC is currently 72\u00b0F and sunny. Also, 5 plus 3 equals 8." + } + ], + "role": "assistant" + } + ], + "parallel_tool_calls": true, + "previous_response_id": null, + "prompt_cache_key": null, + "prompt_cache_retention": null, + "reasoning": { + "effort": null, + "summary": null + }, + "safety_identifier": null, + "service_tier": "default", + "store": true, + "temperature": 1.0, + "text": { + "format": { + "type": "text" + }, + "verbosity": "medium" + }, + "tool_choice": "auto", + "tools": [ + { + "type": "function", + "description": "Get the current weather", + "name": "get_weather", + "parameters": { + "type": "object", + "properties": { + "location": { + "type": "string" + } + }, + "required": [ + "location" + ], + "additionalProperties": false + }, + "strict": true + }, + { + "type": "function", + "description": "Perform basic arithmetic", + "name": "calculate", + "parameters": { + "type": "object", + "properties": { + "operation": { + "type": "string", + "enum": [ + "add", + "subtract", + "multiply", + "divide" + ] + }, + "a": { + "type": "number" + }, + "b": { + "type": "number" + } + }, + "required": [ + "operation", + "a", + "b" + ], + "additionalProperties": false + }, + "strict": true + } + ], + "top_logprobs": 0, + "top_p": 1.0, + "truncation": "disabled", + "usage": { + "input_tokens": 167, + "input_tokens_details": { + "cached_tokens": 0 + }, + "output_tokens": 25, + "output_tokens_details": { + "reasoning_tokens": 0 + }, + "total_tokens": 192 + }, + "user": null, + "metadata": {} + } + recorded_at: Wed, 19 Nov 2025 06:11:11 GMT +recorded_with: VCR 6.3.1 diff --git a/test/fixtures/vcr_cassettes/integration/open_ai/responses/common_format/tools_test/test_agent_common_format_parameters.yml b/test/fixtures/vcr_cassettes/integration/open_ai/responses/common_format/tools_test/test_agent_common_format_parameters.yml new file mode 100644 index 00000000..97cddd30 --- /dev/null +++ b/test/fixtures/vcr_cassettes/integration/open_ai/responses/common_format/tools_test/test_agent_common_format_parameters.yml @@ -0,0 +1,379 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.openai.com/v1/responses + body: + encoding: UTF-8 + string: '{"model":"gpt-4.1","input":"What''s the weather in San Francisco?","tools":[{"type":"function","name":"get_weather","description":"Get + the current weather in a given location","parameters":{"type":"object","properties":{"location":{"type":"string","description":"The + city and state, e.g. San Francisco, CA"}},"required":["location"]}}],"tool_choice":"auto"}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json + User-Agent: + - OpenAI::Client/Ruby 0.35.1 + Host: + - api.openai.com + X-Stainless-Arch: + - arm64 + X-Stainless-Lang: + - ruby + X-Stainless-Os: + - MacOS + X-Stainless-Package-Version: + - 0.35.1 + X-Stainless-Runtime: + - ruby + X-Stainless-Runtime-Version: + - 3.4.7 + Content-Type: + - application/json + Openai-Organization: + - ORGANIZATION_ID + Openai-Project: + - PROJECT_ID + Authorization: + - Bearer ACCESS_TOKEN + X-Stainless-Retry-Count: + - '0' + X-Stainless-Timeout: + - '600.0' + Content-Length: + - '356' + response: + status: + code: 200 + message: OK + headers: + Date: + - Wed, 19 Nov 2025 06:11:13 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + X-Ratelimit-Limit-Requests: + - '10000' + X-Ratelimit-Limit-Tokens: + - '30000000' + X-Ratelimit-Remaining-Requests: + - '9999' + X-Ratelimit-Remaining-Tokens: + - '29999722' + X-Ratelimit-Reset-Requests: + - 6ms + X-Ratelimit-Reset-Tokens: + - 0s + Openai-Version: + - '2020-10-01' + Openai-Organization: + - ORGANIZATION_ID + Openai-Project: + - PROJECT_ID + X-Request-Id: + - req_e98adf069fbb426ea7a05f4d066c8323 + Openai-Processing-Ms: + - '673' + X-Envoy-Upstream-Service-Time: + - '675' + Cf-Cache-Status: + - DYNAMIC + Set-Cookie: + - __cf_bm=G4mTIIHTZVf3x_nD0fNFbP1TYYAKLc0tyi5MqyWKXus-1763532673-1.0.1.1-0lNJ4.ywfxbHgQviNhfK76eX3bRObUADyXH84E6SB1mwDRai7MuAyuJWdRCBxsQ6GsdoUGZXtbemqQrYV5iPN7nf.hPsFGy6kw8dBoSQ5fY; + path=/; expires=Wed, 19-Nov-25 06:41:13 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=OxrjL31xuRt.LmXIwAh4msKrf779HRAFIPUalLcXVXg-1763532673412-0.0.1.1-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + X-Content-Type-Options: + - nosniff + Server: + - cloudflare + Cf-Ray: + - 9a0d8c84591015b4-SJC + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: |- + { + "id": "resp_05b9e4590cdecec900691d5f80be748199838550444bf1fad6", + "object": "response", + "created_at": 1763532672, + "status": "completed", + "background": false, + "billing": { + "payer": "developer" + }, + "error": null, + "incomplete_details": null, + "instructions": null, + "max_output_tokens": null, + "max_tool_calls": null, + "model": "gpt-4.1-2025-04-14", + "output": [ + { + "id": "fc_05b9e4590cdecec900691d5f81343481999661e86ddef850c1", + "type": "function_call", + "status": "completed", + "arguments": "{\"location\":\"San Francisco, CA\"}", + "call_id": "call_lRBYXQbTLjY8GvUsBHWt9yqR", + "name": "get_weather" + } + ], + "parallel_tool_calls": true, + "previous_response_id": null, + "prompt_cache_key": null, + "prompt_cache_retention": null, + "reasoning": { + "effort": null, + "summary": null + }, + "safety_identifier": null, + "service_tier": "default", + "store": true, + "temperature": 1.0, + "text": { + "format": { + "type": "text" + }, + "verbosity": "medium" + }, + "tool_choice": "auto", + "tools": [ + { + "type": "function", + "description": "Get the current weather in a given location", + "name": "get_weather", + "parameters": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "The city and state, e.g. San Francisco, CA" + } + }, + "required": [ + "location" + ], + "additionalProperties": false + }, + "strict": true + } + ], + "top_logprobs": 0, + "top_p": 1.0, + "truncation": "disabled", + "usage": { + "input_tokens": 61, + "input_tokens_details": { + "cached_tokens": 0 + }, + "output_tokens": 18, + "output_tokens_details": { + "reasoning_tokens": 0 + }, + "total_tokens": 79 + }, + "user": null, + "metadata": {} + } + recorded_at: Wed, 19 Nov 2025 06:11:13 GMT +- request: + method: post + uri: https://api.openai.com/v1/responses + body: + encoding: UTF-8 + string: '{"model":"gpt-4.1","input":[{"role":"user","content":"What''s the weather + in San Francisco?"},{"arguments":"{\"location\":\"San Francisco, CA\"}","call_id":"call_lRBYXQbTLjY8GvUsBHWt9yqR","name":"get_weather","type":"function_call","id":"fc_05b9e4590cdecec900691d5f81343481999661e86ddef850c1","status":"completed"},{"call_id":"call_lRBYXQbTLjY8GvUsBHWt9yqR","output":"{\"location\":\"San + Francisco, CA\",\"temperature\":\"72°F\",\"conditions\":\"sunny\"}","type":"function_call_output"}],"tools":[{"type":"function","name":"get_weather","description":"Get + the current weather in a given location","parameters":{"type":"object","properties":{"location":{"type":"string","description":"The + city and state, e.g. San Francisco, CA"}},"required":["location"]}}],"tool_choice":"auto"}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json + User-Agent: + - OpenAI::Client/Ruby 0.35.1 + Host: + - api.openai.com + X-Stainless-Arch: + - arm64 + X-Stainless-Lang: + - ruby + X-Stainless-Os: + - MacOS + X-Stainless-Package-Version: + - 0.35.1 + X-Stainless-Runtime: + - ruby + X-Stainless-Runtime-Version: + - 3.4.7 + Content-Type: + - application/json + Openai-Organization: + - ORGANIZATION_ID + Openai-Project: + - PROJECT_ID + Authorization: + - Bearer ACCESS_TOKEN + X-Stainless-Retry-Count: + - '0' + X-Stainless-Timeout: + - '600.0' + Content-Length: + - '778' + response: + status: + code: 200 + message: OK + headers: + Date: + - Wed, 19 Nov 2025 06:11:14 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + X-Ratelimit-Limit-Requests: + - '10000' + X-Ratelimit-Limit-Tokens: + - '30000000' + X-Ratelimit-Remaining-Requests: + - '9999' + X-Ratelimit-Remaining-Tokens: + - '29999678' + X-Ratelimit-Reset-Requests: + - 6ms + X-Ratelimit-Reset-Tokens: + - 0s + Openai-Version: + - '2020-10-01' + Openai-Organization: + - ORGANIZATION_ID + Openai-Project: + - PROJECT_ID + X-Request-Id: + - req_1574a48a1716446cbbc82375a4691667 + Openai-Processing-Ms: + - '705' + X-Envoy-Upstream-Service-Time: + - '708' + Cf-Cache-Status: + - DYNAMIC + Set-Cookie: + - __cf_bm=7mNinPBkS7_N3DT0HA.ZNHLXSwniIU15U0pnIVNn_Gs-1763532674-1.0.1.1-v4HotQ8JHGZoxBJa66yMEVDWE7eZsbtQkk7ntalWLwkAvWmAFqYW3rnK6FlaD.nUFHZbXZ_qv1nqwfumx3OBmnOWzWF28qUe_CicxJufKPQ; + path=/; expires=Wed, 19-Nov-25 06:41:14 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=OLWAssiS01iK.fjQDmE7ByKsj_n6aW0I2iPEVxA4ajQ-1763532674244-0.0.1.1-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + X-Content-Type-Options: + - nosniff + Server: + - cloudflare + Cf-Ray: + - 9a0d8c8929c5ed3b-SJC + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: |- + { + "id": "resp_05b9e4590cdecec900691d5f81874c819993de1c6ea20575b6", + "object": "response", + "created_at": 1763532673, + "status": "completed", + "background": false, + "billing": { + "payer": "developer" + }, + "error": null, + "incomplete_details": null, + "instructions": null, + "max_output_tokens": null, + "max_tool_calls": null, + "model": "gpt-4.1-2025-04-14", + "output": [ + { + "id": "msg_05b9e4590cdecec900691d5f81e1c88199b5089bf73ae19a62", + "type": "message", + "status": "completed", + "content": [ + { + "type": "output_text", + "annotations": [], + "logprobs": [], + "text": "The weather in San Francisco is currently sunny with a temperature of 72\u00b0F." + } + ], + "role": "assistant" + } + ], + "parallel_tool_calls": true, + "previous_response_id": null, + "prompt_cache_key": null, + "prompt_cache_retention": null, + "reasoning": { + "effort": null, + "summary": null + }, + "safety_identifier": null, + "service_tier": "default", + "store": true, + "temperature": 1.0, + "text": { + "format": { + "type": "text" + }, + "verbosity": "medium" + }, + "tool_choice": "auto", + "tools": [ + { + "type": "function", + "description": "Get the current weather in a given location", + "name": "get_weather", + "parameters": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "The city and state, e.g. San Francisco, CA" + } + }, + "required": [ + "location" + ], + "additionalProperties": false + }, + "strict": true + } + ], + "top_logprobs": 0, + "top_p": 1.0, + "truncation": "disabled", + "usage": { + "input_tokens": 105, + "input_tokens_details": { + "cached_tokens": 0 + }, + "output_tokens": 18, + "output_tokens_details": { + "reasoning_tokens": 0 + }, + "total_tokens": 123 + }, + "user": null, + "metadata": {} + } + recorded_at: Wed, 19 Nov 2025 06:11:14 GMT +recorded_with: VCR 6.3.1 diff --git a/test/fixtures/vcr_cassettes/integration/open_ai/responses/common_format/tools_test/test_agent_common_format_tool_choice_auto.yml b/test/fixtures/vcr_cassettes/integration/open_ai/responses/common_format/tools_test/test_agent_common_format_tool_choice_auto.yml new file mode 100644 index 00000000..d3bccf47 --- /dev/null +++ b/test/fixtures/vcr_cassettes/integration/open_ai/responses/common_format/tools_test/test_agent_common_format_tool_choice_auto.yml @@ -0,0 +1,191 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.openai.com/v1/responses + body: + encoding: UTF-8 + string: '{"model":"gpt-4.1","input":"What''s the weather?","tools":[{"type":"function","name":"get_weather","description":"Get + weather","parameters":{"type":"object","properties":{"location":{"type":"string"}},"required":["location"]}}],"tool_choice":"auto"}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json + User-Agent: + - OpenAI::Client/Ruby 0.35.1 + Host: + - api.openai.com + X-Stainless-Arch: + - arm64 + X-Stainless-Lang: + - ruby + X-Stainless-Os: + - MacOS + X-Stainless-Package-Version: + - 0.35.1 + X-Stainless-Runtime: + - ruby + X-Stainless-Runtime-Version: + - 3.4.7 + Content-Type: + - application/json + Openai-Organization: + - ORGANIZATION_ID + Openai-Project: + - PROJECT_ID + Authorization: + - Bearer ACCESS_TOKEN + X-Stainless-Retry-Count: + - '0' + X-Stainless-Timeout: + - '600.0' + Content-Length: + - '248' + response: + status: + code: 200 + message: OK + headers: + Date: + - Wed, 19 Nov 2025 06:11:12 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + X-Ratelimit-Limit-Requests: + - '10000' + X-Ratelimit-Limit-Tokens: + - '30000000' + X-Ratelimit-Remaining-Requests: + - '9999' + X-Ratelimit-Remaining-Tokens: + - '29999745' + X-Ratelimit-Reset-Requests: + - 6ms + X-Ratelimit-Reset-Tokens: + - 0s + Openai-Version: + - '2020-10-01' + Openai-Organization: + - ORGANIZATION_ID + Openai-Project: + - PROJECT_ID + X-Request-Id: + - req_abac2de70ba941569fd44a3be39d40b7 + Openai-Processing-Ms: + - '1149' + X-Envoy-Upstream-Service-Time: + - '1155' + Cf-Cache-Status: + - DYNAMIC + Set-Cookie: + - __cf_bm=vve89jfXkilgqsMV3.99d5pTaZhhxsID_aWVFRY2xQ8-1763532672-1.0.1.1-l5f6mxlE3CwatB7GQBleHgldqOAiNNm1OAq0s.KRkrT4iK0NeB_C5Pzn.Jk7MCFzapLyZ0gYmFcFhQHLQsmGtgDj41qfCDdr_8mCueJD53o; + path=/; expires=Wed, 19-Nov-25 06:41:12 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=t54Np9cWGfao5kv.0EHIeg5CcBZRYitdVG.Mi1o8Tb0-1763532672615-0.0.1.1-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + X-Content-Type-Options: + - nosniff + Server: + - cloudflare + Cf-Ray: + - 9a0d8c7c69d0cf1f-SJC + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: |- + { + "id": "resp_01938aae9821358a00691d5f7f7ba4819bbc7200a6be080920", + "object": "response", + "created_at": 1763532671, + "status": "completed", + "background": false, + "billing": { + "payer": "developer" + }, + "error": null, + "incomplete_details": null, + "instructions": null, + "max_output_tokens": null, + "max_tool_calls": null, + "model": "gpt-4.1-2025-04-14", + "output": [ + { + "id": "msg_01938aae9821358a00691d5f800e1c819ba7d7722efebb9056", + "type": "message", + "status": "completed", + "content": [ + { + "type": "output_text", + "annotations": [], + "logprobs": [], + "text": "Could you please specify the location for which you want to know the weather?" + } + ], + "role": "assistant" + } + ], + "parallel_tool_calls": true, + "previous_response_id": null, + "prompt_cache_key": null, + "prompt_cache_retention": null, + "reasoning": { + "effort": null, + "summary": null + }, + "safety_identifier": null, + "service_tier": "default", + "store": true, + "temperature": 1.0, + "text": { + "format": { + "type": "text" + }, + "verbosity": "medium" + }, + "tool_choice": "auto", + "tools": [ + { + "type": "function", + "description": "Get weather", + "name": "get_weather", + "parameters": { + "type": "object", + "properties": { + "location": { + "type": "string" + } + }, + "required": [ + "location" + ], + "additionalProperties": false + }, + "strict": true + } + ], + "top_logprobs": 0, + "top_p": 1.0, + "truncation": "disabled", + "usage": { + "input_tokens": 38, + "input_tokens_details": { + "cached_tokens": 0 + }, + "output_tokens": 17, + "output_tokens_details": { + "reasoning_tokens": 0 + }, + "total_tokens": 55 + }, + "user": null, + "metadata": {} + } + recorded_at: Wed, 19 Nov 2025 06:11:12 GMT +recorded_with: VCR 6.3.1 diff --git a/test/fixtures/vcr_cassettes/integration/open_ai/responses/common_format/tools_test/test_agent_common_format_tool_choice_required.yml b/test/fixtures/vcr_cassettes/integration/open_ai/responses/common_format/tools_test/test_agent_common_format_tool_choice_required.yml new file mode 100644 index 00000000..66ba38de --- /dev/null +++ b/test/fixtures/vcr_cassettes/integration/open_ai/responses/common_format/tools_test/test_agent_common_format_tool_choice_required.yml @@ -0,0 +1,375 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.openai.com/v1/responses + body: + encoding: UTF-8 + string: '{"model":"gpt-4.1","input":"What''s the weather?","tools":[{"type":"function","name":"get_weather","description":"Get + weather","parameters":{"type":"object","properties":{"location":{"type":"string"}},"required":["location"]}}],"tool_choice":"required"}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json + User-Agent: + - OpenAI::Client/Ruby 0.35.1 + Host: + - api.openai.com + X-Stainless-Arch: + - arm64 + X-Stainless-Lang: + - ruby + X-Stainless-Os: + - MacOS + X-Stainless-Package-Version: + - 0.35.1 + X-Stainless-Runtime: + - ruby + X-Stainless-Runtime-Version: + - 3.4.7 + Content-Type: + - application/json + Openai-Organization: + - ORGANIZATION_ID + Openai-Project: + - PROJECT_ID + Authorization: + - Bearer ACCESS_TOKEN + X-Stainless-Retry-Count: + - '0' + X-Stainless-Timeout: + - '600.0' + Content-Length: + - '252' + response: + status: + code: 200 + message: OK + headers: + Date: + - Wed, 19 Nov 2025 06:11:05 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + X-Ratelimit-Limit-Requests: + - '10000' + X-Ratelimit-Limit-Tokens: + - '30000000' + X-Ratelimit-Remaining-Requests: + - '9999' + X-Ratelimit-Remaining-Tokens: + - '29999736' + X-Ratelimit-Reset-Requests: + - 6ms + X-Ratelimit-Reset-Tokens: + - 0s + Openai-Version: + - '2020-10-01' + Openai-Organization: + - ORGANIZATION_ID + Openai-Project: + - PROJECT_ID + X-Request-Id: + - req_b39af137302747a2809a4261a8abea20 + Openai-Processing-Ms: + - '818' + X-Envoy-Upstream-Service-Time: + - '824' + Cf-Cache-Status: + - DYNAMIC + Set-Cookie: + - __cf_bm=1iNRNtfbceMffE6..cNAVlJobes1ggv.O1iuv1HvrzE-1763532665-1.0.1.1-t2_aLgoSgZH7s8a8fze8vYej5xUhB2IEi887ZW3qoGUj0tYyNblOB7sLg5mPO_4fs44ODXAOx2VKNMRSgUYHhFCDKzr0Oc7YWK8V0Hw97jk; + path=/; expires=Wed, 19-Nov-25 06:41:05 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=FwFeiyea_FDkSdlfNlNE4mr1WNurgkaOM3MP.SBv.YM-1763532665969-0.0.1.1-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + X-Content-Type-Options: + - nosniff + Server: + - cloudflare + Cf-Ray: + - 9a0d8c54aed31f2f-SJC + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: |- + { + "id": "resp_05939eef746481bd00691d5f7921848198990d68390590d2c6", + "object": "response", + "created_at": 1763532665, + "status": "completed", + "background": false, + "billing": { + "payer": "developer" + }, + "error": null, + "incomplete_details": null, + "instructions": null, + "max_output_tokens": null, + "max_tool_calls": null, + "model": "gpt-4.1-2025-04-14", + "output": [ + { + "id": "fc_05939eef746481bd00691d5f799ee88198bd7e244498e2a1b8", + "type": "function_call", + "status": "completed", + "arguments": "{\"location\":\"current location\"}", + "call_id": "call_kios4Y7iAP7MSxok1Y0WfaBi", + "name": "get_weather" + } + ], + "parallel_tool_calls": true, + "previous_response_id": null, + "prompt_cache_key": null, + "prompt_cache_retention": null, + "reasoning": { + "effort": null, + "summary": null + }, + "safety_identifier": null, + "service_tier": "default", + "store": true, + "temperature": 1.0, + "text": { + "format": { + "type": "text" + }, + "verbosity": "medium" + }, + "tool_choice": "required", + "tools": [ + { + "type": "function", + "description": "Get weather", + "name": "get_weather", + "parameters": { + "type": "object", + "properties": { + "location": { + "type": "string" + } + }, + "required": [ + "location" + ], + "additionalProperties": false + }, + "strict": true + } + ], + "top_logprobs": 0, + "top_p": 1.0, + "truncation": "disabled", + "usage": { + "input_tokens": 47, + "input_tokens_details": { + "cached_tokens": 0 + }, + "output_tokens": 7, + "output_tokens_details": { + "reasoning_tokens": 0 + }, + "total_tokens": 54 + }, + "user": null, + "metadata": {} + } + recorded_at: Wed, 19 Nov 2025 06:11:05 GMT +- request: + method: post + uri: https://api.openai.com/v1/responses + body: + encoding: UTF-8 + string: '{"model":"gpt-4.1","input":[{"role":"user","content":"What''s the weather?"},{"arguments":"{\"location\":\"current + location\"}","call_id":"call_kios4Y7iAP7MSxok1Y0WfaBi","name":"get_weather","type":"function_call","id":"fc_05939eef746481bd00691d5f799ee88198bd7e244498e2a1b8","status":"completed"},{"call_id":"call_kios4Y7iAP7MSxok1Y0WfaBi","output":"{\"location\":\"current + location\",\"temperature\":\"72°F\",\"conditions\":\"sunny\"}","type":"function_call_output"}],"tools":[{"type":"function","name":"get_weather","description":"Get + weather","parameters":{"type":"object","properties":{"location":{"type":"string"}},"required":["location"]}}],"tool_choice":null}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json + User-Agent: + - OpenAI::Client/Ruby 0.35.1 + Host: + - api.openai.com + X-Stainless-Arch: + - arm64 + X-Stainless-Lang: + - ruby + X-Stainless-Os: + - MacOS + X-Stainless-Package-Version: + - 0.35.1 + X-Stainless-Runtime: + - ruby + X-Stainless-Runtime-Version: + - 3.4.7 + Content-Type: + - application/json + Openai-Organization: + - ORGANIZATION_ID + Openai-Project: + - PROJECT_ID + Authorization: + - Bearer ACCESS_TOKEN + X-Stainless-Retry-Count: + - '0' + X-Stainless-Timeout: + - '600.0' + Content-Length: + - '666' + response: + status: + code: 200 + message: OK + headers: + Date: + - Wed, 19 Nov 2025 06:11:07 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + X-Ratelimit-Limit-Requests: + - '10000' + X-Ratelimit-Limit-Tokens: + - '30000000' + X-Ratelimit-Remaining-Requests: + - '9999' + X-Ratelimit-Remaining-Tokens: + - '29999708' + X-Ratelimit-Reset-Requests: + - 6ms + X-Ratelimit-Reset-Tokens: + - 0s + Openai-Version: + - '2020-10-01' + Openai-Organization: + - ORGANIZATION_ID + Openai-Project: + - PROJECT_ID + X-Request-Id: + - req_51068511f5574ea3b3043e8a3948b814 + Openai-Processing-Ms: + - '943' + X-Envoy-Upstream-Service-Time: + - '946' + Cf-Cache-Status: + - DYNAMIC + Set-Cookie: + - __cf_bm=KXDTmj15Qrw0tabWDXcG42522POTu0p46gOGV9yd32c-1763532667-1.0.1.1-WuY909ShUoXvjLXo2XBeotf6CjpnfflNqsZaorFI1dO3H.YSV9aLja4JH1ZXlCEFXE5Atw86oBj.L46McgkC_sYrphBqxcqqqAvjmlr_ksM; + path=/; expires=Wed, 19-Nov-25 06:41:07 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=38f7IUJwhJPbUHcyMlmaZGjuKrKgoXx1xhcd4uuvHlw-1763532667019-0.0.1.1-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + X-Content-Type-Options: + - nosniff + Server: + - cloudflare + Cf-Ray: + - 9a0d8c5aba03cfd9-SJC + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: |- + { + "id": "resp_05939eef746481bd00691d5f7a161c819885d59a0f70306e70", + "object": "response", + "created_at": 1763532666, + "status": "completed", + "background": false, + "billing": { + "payer": "developer" + }, + "error": null, + "incomplete_details": null, + "instructions": null, + "max_output_tokens": null, + "max_tool_calls": null, + "model": "gpt-4.1-2025-04-14", + "output": [ + { + "id": "msg_05939eef746481bd00691d5f7a68f881989a296625c7190d01", + "type": "message", + "status": "completed", + "content": [ + { + "type": "output_text", + "annotations": [], + "logprobs": [], + "text": "The current weather is sunny with a temperature of 72\u00b0F. If you\u2019d like the weather for a specific location, just let me know!" + } + ], + "role": "assistant" + } + ], + "parallel_tool_calls": true, + "previous_response_id": null, + "prompt_cache_key": null, + "prompt_cache_retention": null, + "reasoning": { + "effort": null, + "summary": null + }, + "safety_identifier": null, + "service_tier": "default", + "store": true, + "temperature": 1.0, + "text": { + "format": { + "type": "text" + }, + "verbosity": "medium" + }, + "tool_choice": "auto", + "tools": [ + { + "type": "function", + "description": "Get weather", + "name": "get_weather", + "parameters": { + "type": "object", + "properties": { + "location": { + "type": "string" + } + }, + "required": [ + "location" + ], + "additionalProperties": false + }, + "strict": true + } + ], + "top_logprobs": 0, + "top_p": 1.0, + "truncation": "disabled", + "usage": { + "input_tokens": 75, + "input_tokens_details": { + "cached_tokens": 0 + }, + "output_tokens": 31, + "output_tokens_details": { + "reasoning_tokens": 0 + }, + "total_tokens": 106 + }, + "user": null, + "metadata": {} + } + recorded_at: Wed, 19 Nov 2025 06:11:07 GMT +recorded_with: VCR 6.3.1 diff --git a/test/fixtures/vcr_cassettes/integration/open_ai/responses/common_format/tools_test/test_agent_common_format_tool_choice_specific.yml b/test/fixtures/vcr_cassettes/integration/open_ai/responses/common_format/tools_test/test_agent_common_format_tool_choice_specific.yml new file mode 100644 index 00000000..2b248247 --- /dev/null +++ b/test/fixtures/vcr_cassettes/integration/open_ai/responses/common_format/tools_test/test_agent_common_format_tool_choice_specific.yml @@ -0,0 +1,378 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.openai.com/v1/responses + body: + encoding: UTF-8 + string: '{"model":"gpt-4.1","input":"What''s the weather?","tools":[{"type":"function","name":"get_weather","description":"Get + weather","parameters":{"type":"object","properties":{"location":{"type":"string"}},"required":["location"]}}],"tool_choice":{"type":"function","name":"get_weather"}}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json + User-Agent: + - OpenAI::Client/Ruby 0.35.1 + Host: + - api.openai.com + X-Stainless-Arch: + - arm64 + X-Stainless-Lang: + - ruby + X-Stainless-Os: + - MacOS + X-Stainless-Package-Version: + - 0.35.1 + X-Stainless-Runtime: + - ruby + X-Stainless-Runtime-Version: + - 3.4.7 + Content-Type: + - application/json + Openai-Organization: + - ORGANIZATION_ID + Openai-Project: + - PROJECT_ID + Authorization: + - Bearer ACCESS_TOKEN + X-Stainless-Retry-Count: + - '0' + X-Stainless-Timeout: + - '600.0' + Content-Length: + - '282' + response: + status: + code: 200 + message: OK + headers: + Date: + - Wed, 19 Nov 2025 06:11:15 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + X-Ratelimit-Limit-Requests: + - '10000' + X-Ratelimit-Limit-Tokens: + - '30000000' + X-Ratelimit-Remaining-Requests: + - '9999' + X-Ratelimit-Remaining-Tokens: + - '29999736' + X-Ratelimit-Reset-Requests: + - 6ms + X-Ratelimit-Reset-Tokens: + - 0s + Openai-Version: + - '2020-10-01' + Openai-Organization: + - ORGANIZATION_ID + Openai-Project: + - PROJECT_ID + X-Request-Id: + - req_50f0d2a57ee14fb29d47e4dd4b2d860b + Openai-Processing-Ms: + - '888' + X-Envoy-Upstream-Service-Time: + - '891' + Cf-Cache-Status: + - DYNAMIC + Set-Cookie: + - __cf_bm=iUVByUMfMM5exlVVTJuX3PQw4CFFTWebEZ4qeq_Y2kA-1763532675-1.0.1.1-J8Ea5e8aiRoSGnVvMqTJP8OFR8BLZOMNOb0kqdri_PRKcLil7PtvL_cBzDyqYkVra_keHlOrSQTPpwJBAg6HDwsW65AMrYc50R.b4h1sHHI; + path=/; expires=Wed, 19-Nov-25 06:41:15 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=5GimaK8QHedy.31v3NOh55X3nFosmRNrcmUmAFpN07s-1763532675303-0.0.1.1-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + X-Content-Type-Options: + - nosniff + Server: + - cloudflare + Cf-Ray: + - 9a0d8c8e7d08ed3f-SJC + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: |- + { + "id": "resp_051add7d88bbd5e800691d5f8267a08197ae1be3d521f0f38f", + "object": "response", + "created_at": 1763532674, + "status": "completed", + "background": false, + "billing": { + "payer": "developer" + }, + "error": null, + "incomplete_details": null, + "instructions": null, + "max_output_tokens": null, + "max_tool_calls": null, + "model": "gpt-4.1-2025-04-14", + "output": [ + { + "id": "fc_051add7d88bbd5e800691d5f8305b081979c0c60afb4568e8c", + "type": "function_call", + "status": "completed", + "arguments": "{\"location\":\"your area\"}", + "call_id": "call_qEEg5lgJOgv5HvsQXA3MS7pS", + "name": "get_weather" + } + ], + "parallel_tool_calls": true, + "previous_response_id": null, + "prompt_cache_key": null, + "prompt_cache_retention": null, + "reasoning": { + "effort": null, + "summary": null + }, + "safety_identifier": null, + "service_tier": "default", + "store": true, + "temperature": 1.0, + "text": { + "format": { + "type": "text" + }, + "verbosity": "medium" + }, + "tool_choice": { + "type": "function", + "name": "get_weather" + }, + "tools": [ + { + "type": "function", + "description": "Get weather", + "name": "get_weather", + "parameters": { + "type": "object", + "properties": { + "location": { + "type": "string" + } + }, + "required": [ + "location" + ], + "additionalProperties": false + }, + "strict": true + } + ], + "top_logprobs": 0, + "top_p": 1.0, + "truncation": "disabled", + "usage": { + "input_tokens": 47, + "input_tokens_details": { + "cached_tokens": 0 + }, + "output_tokens": 7, + "output_tokens_details": { + "reasoning_tokens": 0 + }, + "total_tokens": 54 + }, + "user": null, + "metadata": {} + } + recorded_at: Wed, 19 Nov 2025 06:11:15 GMT +- request: + method: post + uri: https://api.openai.com/v1/responses + body: + encoding: UTF-8 + string: '{"model":"gpt-4.1","input":[{"role":"user","content":"What''s the weather?"},{"arguments":"{\"location\":\"your + area\"}","call_id":"call_qEEg5lgJOgv5HvsQXA3MS7pS","name":"get_weather","type":"function_call","id":"fc_051add7d88bbd5e800691d5f8305b081979c0c60afb4568e8c","status":"completed"},{"call_id":"call_qEEg5lgJOgv5HvsQXA3MS7pS","output":"{\"location\":\"your + area\",\"temperature\":\"72°F\",\"conditions\":\"sunny\"}","type":"function_call_output"}],"tools":[{"type":"function","name":"get_weather","description":"Get + weather","parameters":{"type":"object","properties":{"location":{"type":"string"}},"required":["location"]}}],"tool_choice":null}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json + User-Agent: + - OpenAI::Client/Ruby 0.35.1 + Host: + - api.openai.com + X-Stainless-Arch: + - arm64 + X-Stainless-Lang: + - ruby + X-Stainless-Os: + - MacOS + X-Stainless-Package-Version: + - 0.35.1 + X-Stainless-Runtime: + - ruby + X-Stainless-Runtime-Version: + - 3.4.7 + Content-Type: + - application/json + Openai-Organization: + - ORGANIZATION_ID + Openai-Project: + - PROJECT_ID + Authorization: + - Bearer ACCESS_TOKEN + X-Stainless-Retry-Count: + - '0' + X-Stainless-Timeout: + - '600.0' + Content-Length: + - '652' + response: + status: + code: 200 + message: OK + headers: + Date: + - Wed, 19 Nov 2025 06:11:16 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + X-Ratelimit-Limit-Requests: + - '10000' + X-Ratelimit-Limit-Tokens: + - '30000000' + X-Ratelimit-Remaining-Requests: + - '9999' + X-Ratelimit-Remaining-Tokens: + - '29999708' + X-Ratelimit-Reset-Requests: + - 6ms + X-Ratelimit-Reset-Tokens: + - 0s + Openai-Version: + - '2020-10-01' + Openai-Organization: + - ORGANIZATION_ID + Openai-Project: + - PROJECT_ID + X-Request-Id: + - req_3f4ebd0f3b534662a6c7cad9ce4f8f60 + Openai-Processing-Ms: + - '1151' + X-Envoy-Upstream-Service-Time: + - '1157' + Cf-Cache-Status: + - DYNAMIC + Set-Cookie: + - __cf_bm=7eBDu6y6m59GwWvE60dlNrgX2wqpPtfmU.goQ2KL2uQ-1763532676-1.0.1.1-v6N0ml6yaWTQ2NEzdn4ExXHluLgpVq0myBJCP9Rg0rkBvFIyY2kzibn4AALrz_S.37X6L4.0ckntL2Ns9CYqGxvTMF.RKS9nogXTJSLYSpY; + path=/; expires=Wed, 19-Nov-25 06:41:16 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=tYWGME1kTuEicMzJN8qkZ2y_PRtObijZW5NHwNVB6TQ-1763532676582-0.0.1.1-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + X-Content-Type-Options: + - nosniff + Server: + - cloudflare + Cf-Ray: + - 9a0d8c94ec45fa86-SJC + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: |- + { + "id": "resp_051add7d88bbd5e800691d5f836b6c8197b066b5185bd18d12", + "object": "response", + "created_at": 1763532675, + "status": "completed", + "background": false, + "billing": { + "payer": "developer" + }, + "error": null, + "incomplete_details": null, + "instructions": null, + "max_output_tokens": null, + "max_tool_calls": null, + "model": "gpt-4.1-2025-04-14", + "output": [ + { + "id": "msg_051add7d88bbd5e800691d5f8412e88197a00f1726a0b07ae8", + "type": "message", + "status": "completed", + "content": [ + { + "type": "output_text", + "annotations": [], + "logprobs": [], + "text": "The current weather in your area is sunny with a temperature of 72\u00b0F. If you need details for a specific location, let me know!" + } + ], + "role": "assistant" + } + ], + "parallel_tool_calls": true, + "previous_response_id": null, + "prompt_cache_key": null, + "prompt_cache_retention": null, + "reasoning": { + "effort": null, + "summary": null + }, + "safety_identifier": null, + "service_tier": "default", + "store": true, + "temperature": 1.0, + "text": { + "format": { + "type": "text" + }, + "verbosity": "medium" + }, + "tool_choice": "auto", + "tools": [ + { + "type": "function", + "description": "Get weather", + "name": "get_weather", + "parameters": { + "type": "object", + "properties": { + "location": { + "type": "string" + } + }, + "required": [ + "location" + ], + "additionalProperties": false + }, + "strict": true + } + ], + "top_logprobs": 0, + "top_p": 1.0, + "truncation": "disabled", + "usage": { + "input_tokens": 75, + "input_tokens_details": { + "cached_tokens": 0 + }, + "output_tokens": 31, + "output_tokens_details": { + "reasoning_tokens": 0 + }, + "total_tokens": 106 + }, + "user": null, + "metadata": {} + } + recorded_at: Wed, 19 Nov 2025 06:11:16 GMT +recorded_with: VCR 6.3.1 diff --git a/test/integration/open_ai/responses/common_format/tools_test.rb b/test/integration/open_ai/responses/common_format/tools_test.rb new file mode 100644 index 00000000..a9509f48 --- /dev/null +++ b/test/integration/open_ai/responses/common_format/tools_test.rb @@ -0,0 +1,329 @@ +# frozen_string_literal: true + +require_relative "../../../test_helper" + +module Integration + module OpenAI + module Responses + module CommonFormat + class ToolsTest < ActiveSupport::TestCase + include Integration::TestHelper + + class TestAgent < ActiveAgent::Base + generate_with :openai, model: "gpt-4.1" + + def get_weather(location:) + { location: location, temperature: "72°F", conditions: "sunny" } + end + + def calculate(operation:, a:, b:) + result = case operation + when "add" then a + b + when "subtract" then a - b + when "multiply" then a * b + when "divide" then a / b + end + { operation: operation, a: a, b: b, result: result } + end + + # Common format with 'parameters' key (recommended) + COMMON_FORMAT_PARAMETERS = { + model: "gpt-4.1", + input: "What's the weather in San Francisco?", + tools: [ + { + type: "function", + name: "get_weather", + description: "Get the current weather in a given location", + parameters: { + type: "object", + properties: { + location: { + type: "string", + description: "The city and state, e.g. San Francisco, CA" + } + }, + required: [ "location" ] + } + } + ], + tool_choice: "auto" + } + def common_format_parameters + prompt( + input: "What's the weather in San Francisco?", + tools: [ + { + name: "get_weather", + description: "Get the current weather in a given location", + parameters: { + type: "object", + properties: { + location: { + type: "string", + description: "The city and state, e.g. San Francisco, CA" + } + }, + required: [ "location" ] + } + } + ], + tool_choice: "auto" + ) + end + + # Common format with 'input_schema' key (Anthropic-style) + COMMON_FORMAT_INPUT_SCHEMA = { + model: "gpt-4.1", + input: "What's the weather in Boston?", + tools: [ + { + type: "function", + name: "get_weather", + description: "Get the current weather in a given location", + parameters: { + type: "object", + properties: { + location: { + type: "string", + description: "The city and state, e.g. San Francisco, CA" + } + }, + required: [ "location" ] + } + } + ], + tool_choice: "auto" + } + def common_format_input_schema + prompt( + input: "What's the weather in Boston?", + tools: [ + { + name: "get_weather", + description: "Get the current weather in a given location", + input_schema: { + type: "object", + properties: { + location: { + type: "string", + description: "The city and state, e.g. San Francisco, CA" + } + }, + required: [ "location" ] + } + } + ], + tool_choice: "auto" + ) + end + + # Multiple tools in common format + COMMON_FORMAT_MULTIPLE_TOOLS = { + model: "gpt-4.1", + input: "What's the weather in NYC and what's 5 plus 3?", + tools: [ + { + type: "function", + name: "get_weather", + description: "Get the current weather", + parameters: { + type: "object", + properties: { + location: { type: "string" } + }, + required: [ "location" ] + } + }, + { + type: "function", + name: "calculate", + description: "Perform basic arithmetic", + parameters: { + type: "object", + properties: { + operation: { type: "string", enum: [ "add", "subtract", "multiply", "divide" ] }, + a: { type: "number" }, + b: { type: "number" } + }, + required: [ "operation", "a", "b" ] + } + } + ], + tool_choice: "auto" + } + def common_format_multiple_tools + prompt( + input: "What's the weather in NYC and what's 5 plus 3?", + tools: [ + { + name: "get_weather", + description: "Get the current weather", + parameters: { + type: "object", + properties: { + location: { type: "string" } + }, + required: [ "location" ] + } + }, + { + name: "calculate", + description: "Perform basic arithmetic", + parameters: { + type: "object", + properties: { + operation: { type: "string", enum: [ "add", "subtract", "multiply", "divide" ] }, + a: { type: "number" }, + b: { type: "number" } + }, + required: [ "operation", "a", "b" ] + } + } + ], + tool_choice: "auto" + ) + end + + # Tool choice - string format + COMMON_FORMAT_TOOL_CHOICE_AUTO = { + model: "gpt-4.1", + input: "What's the weather?", + tools: [ + { + type: "function", + name: "get_weather", + description: "Get weather", + parameters: { + type: "object", + properties: { + location: { type: "string" } + }, + required: [ "location" ] + } + } + ], + tool_choice: "auto" + } + def common_format_tool_choice_auto + prompt( + input: "What's the weather?", + tools: [ + { + name: "get_weather", + description: "Get weather", + parameters: { + type: "object", + properties: { + location: { type: "string" } + }, + required: [ "location" ] + } + } + ], + tool_choice: "auto" + ) + end + + # Tool choice - force tool use with "required" + COMMON_FORMAT_TOOL_CHOICE_REQUIRED = { + model: "gpt-4.1", + input: "What's the weather?", + tools: [ + { + type: "function", + name: "get_weather", + description: "Get weather", + parameters: { + type: "object", + properties: { + location: { type: "string" } + }, + required: [ "location" ] + } + } + ], + tool_choice: "required" + } + def common_format_tool_choice_required + prompt( + input: "What's the weather?", + tools: [ + { + name: "get_weather", + description: "Get weather", + parameters: { + type: "object", + properties: { + location: { type: "string" } + }, + required: [ "location" ] + } + } + ], + tool_choice: "required" + ) + end + + # Tool choice - specific tool + COMMON_FORMAT_TOOL_CHOICE_SPECIFIC = { + model: "gpt-4.1", + input: "What's the weather?", + tools: [ + { + type: "function", + name: "get_weather", + description: "Get weather", + parameters: { + type: "object", + properties: { + location: { type: "string" } + }, + required: [ "location" ] + } + } + ], + tool_choice: { + type: "function", + name: "get_weather" + } + } + def common_format_tool_choice_specific + prompt( + input: "What's the weather?", + tools: [ + { + name: "get_weather", + description: "Get weather", + parameters: { + type: "object", + properties: { + location: { type: "string" } + }, + required: [ "location" ] + } + } + ], + tool_choice: { name: "get_weather" } + ) + end + end + + ################################################################################ + # This automatically runs all the tests for the test actions + ################################################################################ + [ + :common_format_parameters, + :common_format_input_schema, + :common_format_multiple_tools, + :common_format_tool_choice_auto, + :common_format_tool_choice_required, + :common_format_tool_choice_specific + ].each do |action_name| + test_request_builder(TestAgent, action_name, :generate_now, TestAgent.const_get(action_name.to_s.upcase, true)) + end + end + end + end + end +end From 9a5a1c740dfa0f0a2f7f0cf829ad3457d1097d12 Mon Sep 17 00:00:00 2001 From: Zane Wolfgang Pickett Date: Tue, 18 Nov 2025 22:20:23 -0800 Subject: [PATCH 03/17] Add OpenAI Chat Tools Common Format --- .../providers/open_ai/chat/transforms.rb | 86 +++- .../providers/open_ai/chat_provider.rb | 34 ++ .../test_agent_common_format_input_schema.yml | 266 ++++++++++++ ...est_agent_common_format_multiple_tools.yml | 275 +++++++++++++ .../test_agent_common_format_parameters.yml | 266 ++++++++++++ ...t_agent_common_format_tool_choice_auto.yml | 145 +++++++ ...ent_common_format_tool_choice_required.yml | 264 ++++++++++++ ...ent_common_format_tool_choice_specific.yml | 264 ++++++++++++ .../open_ai/chat/common_format/tools_test.rb | 384 ++++++++++++++++++ 9 files changed, 1981 insertions(+), 3 deletions(-) create mode 100644 test/fixtures/vcr_cassettes/integration/open_ai/chat/common_format/tools_test/test_agent_common_format_input_schema.yml create mode 100644 test/fixtures/vcr_cassettes/integration/open_ai/chat/common_format/tools_test/test_agent_common_format_multiple_tools.yml create mode 100644 test/fixtures/vcr_cassettes/integration/open_ai/chat/common_format/tools_test/test_agent_common_format_parameters.yml create mode 100644 test/fixtures/vcr_cassettes/integration/open_ai/chat/common_format/tools_test/test_agent_common_format_tool_choice_auto.yml create mode 100644 test/fixtures/vcr_cassettes/integration/open_ai/chat/common_format/tools_test/test_agent_common_format_tool_choice_required.yml create mode 100644 test/fixtures/vcr_cassettes/integration/open_ai/chat/common_format/tools_test/test_agent_common_format_tool_choice_specific.yml create mode 100644 test/integration/open_ai/chat/common_format/tools_test.rb diff --git a/lib/active_agent/providers/open_ai/chat/transforms.rb b/lib/active_agent/providers/open_ai/chat/transforms.rb index 42e1d71d..bb24b699 100644 --- a/lib/active_agent/providers/open_ai/chat/transforms.rb +++ b/lib/active_agent/providers/open_ai/chat/transforms.rb @@ -24,8 +24,8 @@ def gem_to_hash(gem_object) # Normalizes all request parameters for OpenAI Chat API # # Handles instructions mapping to developer messages, message normalization, - # and response_format conversion. This is the main entry point for parameter - # transformation. + # tools normalization, and response_format conversion. This is the main entry point + # for parameter transformation. # # @param params [Hash] # @return [Hash] normalized parameters @@ -41,6 +41,12 @@ def normalize_params(params) # Normalize messages for gem compatibility params[:messages] = normalize_messages(params[:messages]) if params[:messages] + # Normalize tools from common format to Chat API format + params[:tools] = normalize_tools(params[:tools]) if params[:tools] + + # Normalize tool_choice from common format + params[:tool_choice] = normalize_tool_choice(params[:tool_choice]) if params[:tool_choice] + # Normalize response_format if present params[:response_format] = normalize_response_format(params[:response_format]) if params[:response_format] @@ -68,7 +74,8 @@ def normalize_messages(messages) messages.each do |msg| normalized = normalize_message(msg) - if grouped.empty? || grouped.last.role != normalized.role + # Don't merge tool messages - each needs its own tool_call_id + if grouped.empty? || grouped.last.role != normalized.role || normalized.role.to_s == "tool" grouped << normalized else # Merge consecutive same-role messages @@ -307,6 +314,79 @@ def normalize_response_format(format) end end + # Normalizes tools from common format to OpenAI Chat API format. + # + # Accepts tools in multiple formats: + # - Common format: `{name: "...", description: "...", parameters: {...}}` + # - Common format alt: `{name: "...", description: "...", input_schema: {...}}` + # - Nested format: `{type: "function", function: {name: "...", parameters: {...}}}` + # + # Always outputs nested Chat API format: `{type: "function", function: {...}}` + # + # @param tools [Array] + # @return [Array] + def normalize_tools(tools) + return tools unless tools.is_a?(Array) + + tools.map do |tool| + tool_hash = tool.is_a?(Hash) ? tool.deep_symbolize_keys : tool + + # Already in nested format - return as is + if tool_hash[:type] == "function" && tool_hash[:function] + tool_hash + # Common format - convert to nested format + elsif tool_hash[:name] + { + type: "function", + function: { + name: tool_hash[:name], + description: tool_hash[:description], + parameters: tool_hash[:parameters] || tool_hash[:input_schema] + }.compact + } + else + tool_hash + end + end + end + + # Normalizes tool_choice from common format to OpenAI Chat API format. + # + # Accepts: + # - "auto" (common) → "auto" (passthrough) + # - "required" (common) → "required" (passthrough) + # - `{name: "..."}` (common) → `{type: "function", function: {name: "..."}}` + # - Already nested format → passthrough + # + # @param tool_choice [String, Hash, Symbol] + # @return [String, Hash, Symbol] + def normalize_tool_choice(tool_choice) + case tool_choice + when "auto", :auto, "required", :required + # Passthrough - Chat API accepts these directly + tool_choice.to_s + when Hash + tool_choice_hash = tool_choice.deep_symbolize_keys + + # Already in nested format with type and function keys + if tool_choice_hash[:type] == "function" && tool_choice_hash[:function] + tool_choice_hash + # Common format with just name - convert to nested format + elsif tool_choice_hash[:name] + { + type: "function", + function: { + name: tool_choice_hash[:name] + } + } + else + tool_choice_hash + end + else + tool_choice + end + end + # Normalizes instructions to developer message format # # Converts instructions into developer messages with proper content structure. diff --git a/lib/active_agent/providers/open_ai/chat_provider.rb b/lib/active_agent/providers/open_ai/chat_provider.rb index 75933901..5905ad2e 100644 --- a/lib/active_agent/providers/open_ai/chat_provider.rb +++ b/lib/active_agent/providers/open_ai/chat_provider.rb @@ -30,6 +30,40 @@ def api_prompt_executer client.chat.completions end + # @see BaseProvider#prepare_prompt_request + # @return [Request] + def prepare_prompt_request + prepare_prompt_request_tools + super + end + + # @api private + def prepare_prompt_request_tools + return unless request.tool_choice + + # Get list of function calls that have been made + # In Chat API, tool calls are in the assistant message's tool_calls array + functions_used = message_stack + .select { |msg| msg[:role] == "assistant" && msg[:tool_calls] } + .flat_map { |msg| msg[:tool_calls] } + .map { |tc| tc.dig(:function, :name) } + .compact + + # Check if tool_choice is a hash (specific tool) or string (auto/required) + if request.tool_choice.is_a?(Hash) + # Specific tool choice - clear if that tool was used + tool_choice_name = request.tool_choice.dig(:function, :name) + if tool_choice_name && functions_used.include?(tool_choice_name) + request.tool_choice = nil + end + elsif request.tool_choice == "required" + # Required tool choice - clear if any tool was used + if functions_used.any? + request.tool_choice = nil + end + end + end + # @see BaseProvider#api_response_normalize # @param api_response [OpenAI::Models::ChatCompletion] # @return [Hash] normalized response hash diff --git a/test/fixtures/vcr_cassettes/integration/open_ai/chat/common_format/tools_test/test_agent_common_format_input_schema.yml b/test/fixtures/vcr_cassettes/integration/open_ai/chat/common_format/tools_test/test_agent_common_format_input_schema.yml new file mode 100644 index 00000000..ccde4796 --- /dev/null +++ b/test/fixtures/vcr_cassettes/integration/open_ai/chat/common_format/tools_test/test_agent_common_format_input_schema.yml @@ -0,0 +1,266 @@ +--- +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 weather in Boston?"}],"tools":[{"type":"function","function":{"name":"get_weather","description":"Get + the current weather in a given location","parameters":{"type":"object","properties":{"location":{"type":"string","description":"The + city and state, e.g. San Francisco, CA"}},"required":["location"]}}}]}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json + User-Agent: + - OpenAI::Client/Ruby 0.35.1 + Host: + - api.openai.com + X-Stainless-Arch: + - arm64 + X-Stainless-Lang: + - ruby + X-Stainless-Os: + - MacOS + X-Stainless-Package-Version: + - 0.35.1 + X-Stainless-Runtime: + - ruby + X-Stainless-Runtime-Version: + - 3.4.7 + Content-Type: + - application/json + Openai-Organization: + - ORGANIZATION_ID + Openai-Project: + - PROJECT_ID + Authorization: + - Bearer ACCESS_TOKEN + X-Stainless-Retry-Count: + - '0' + X-Stainless-Timeout: + - '600.0' + Content-Length: + - '376' + response: + status: + code: 200 + message: OK + headers: + Date: + - Wed, 19 Nov 2025 06:19:40 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Access-Control-Expose-Headers: + - X-Request-ID + Openai-Organization: + - ORGANIZATION_ID + Openai-Processing-Ms: + - '570' + Openai-Project: + - PROJECT_ID + Openai-Version: + - '2020-10-01' + X-Envoy-Upstream-Service-Time: + - '583' + X-Ratelimit-Limit-Requests: + - '30000' + X-Ratelimit-Limit-Tokens: + - '150000000' + X-Ratelimit-Remaining-Requests: + - '29999' + X-Ratelimit-Remaining-Tokens: + - '149999990' + X-Ratelimit-Reset-Requests: + - 2ms + X-Ratelimit-Reset-Tokens: + - 0s + X-Request-Id: + - req_2e8785ee0f3e48428b408c0743c4cefa + X-Openai-Proxy-Wasm: + - v0.1 + Cf-Cache-Status: + - DYNAMIC + Set-Cookie: + - __cf_bm=i1rrR0yVKiIrh2CIzRIVYKUU6hIt3wDGTOSvmkohsLs-1763533180-1.0.1.1-X6FcBvIlr8K4zU0eQ3hg79iH.sB_ivFXrby2zEvSY8eMM8Ob9RPPdoO6kZ49kDEkB1Q4rSS.qJgAvFEPEbHSHLdRVmFIvQgPRuGq5HpnD7I; + path=/; expires=Wed, 19-Nov-25 06:49:40 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=fJeWpVHtUQBN.20JB.y3oSuc0zZGCHWGFNARyoxSglU-1763533180771-0.0.1.1-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + X-Content-Type-Options: + - nosniff + Server: + - cloudflare + Cf-Ray: + - 9a0d98e7993a67b2-SJC + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: | + { + "id": "chatcmpl-CdVpUCLVDhitsbz3SEYCmlzlAnzYU", + "object": "chat.completion", + "created": 1763533180, + "model": "gpt-4o-mini-2024-07-18", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": null, + "tool_calls": [ + { + "id": "call_5ml0rdF2bbqFWoNL7mYVVXEd", + "type": "function", + "function": { + "name": "get_weather", + "arguments": "{\"location\":\"Boston, MA\"}" + } + } + ], + "refusal": null, + "annotations": [] + }, + "logprobs": null, + "finish_reason": "tool_calls" + } + ], + "usage": { + "prompt_tokens": 66, + "completion_tokens": 16, + "total_tokens": 82, + "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_560af6e559" + } + recorded_at: Wed, 19 Nov 2025 06:19:40 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 weather in Boston?"},{"role":"assistant","refusal":null,"annotations":[],"tool_calls":[{"id":"call_5ml0rdF2bbqFWoNL7mYVVXEd","function":{"arguments":"{\"location\":\"Boston, + MA\"}","name":"get_weather"},"type":"function"}]},{"role":"tool","content":"{\"location\":\"Boston, + MA\",\"temperature\":\"72°F\",\"conditions\":\"sunny\"}","tool_call_id":"call_5ml0rdF2bbqFWoNL7mYVVXEd"}],"tools":[{"type":"function","function":{"name":"get_weather","description":"Get + the current weather in a given location","parameters":{"type":"object","properties":{"location":{"type":"string","description":"The + city and state, e.g. San Francisco, CA"}},"required":["location"]}}}]}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json + User-Agent: + - OpenAI::Client/Ruby 0.35.1 + Host: + - api.openai.com + X-Stainless-Arch: + - arm64 + X-Stainless-Lang: + - ruby + X-Stainless-Os: + - MacOS + X-Stainless-Package-Version: + - 0.35.1 + X-Stainless-Runtime: + - ruby + X-Stainless-Runtime-Version: + - 3.4.7 + Content-Type: + - application/json + Openai-Organization: + - ORGANIZATION_ID + Openai-Project: + - PROJECT_ID + Authorization: + - Bearer ACCESS_TOKEN + X-Stainless-Retry-Count: + - '0' + X-Stainless-Timeout: + - '600.0' + Content-Length: + - '735' + response: + status: + code: 200 + message: OK + headers: + Date: + - Wed, 19 Nov 2025 06:19:41 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Access-Control-Expose-Headers: + - X-Request-ID + Openai-Organization: + - ORGANIZATION_ID + Openai-Processing-Ms: + - '538' + Openai-Project: + - PROJECT_ID + Openai-Version: + - '2020-10-01' + X-Envoy-Upstream-Service-Time: + - '551' + X-Ratelimit-Limit-Requests: + - '30000' + X-Ratelimit-Limit-Tokens: + - '150000000' + X-Ratelimit-Remaining-Requests: + - '29999' + X-Ratelimit-Remaining-Tokens: + - '149999970' + X-Ratelimit-Reset-Requests: + - 2ms + X-Ratelimit-Reset-Tokens: + - 0s + X-Request-Id: + - req_16ba0436dc5a4325956f80ff1dab8665 + X-Openai-Proxy-Wasm: + - v0.1 + Cf-Cache-Status: + - DYNAMIC + Set-Cookie: + - __cf_bm=3vr3.5qq7r7oWpeJ4H9I.Sxe7OJNK7lG3YLM.JurdfI-1763533181-1.0.1.1-VVa0Zj5rKWMs809pecws.aYAxN.gqIygBE4zEkTDs5zudTzNa1nBh0qA4rCDF_Sre0RetJ2YiC0ukVdWZKzlPB5.3ZHwZNQp0YawPLyH5VQ; + path=/; expires=Wed, 19-Nov-25 06:49:41 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=_k_X1Gy7NLBZsFz_Np3TA2sXsdXHYYKctk.79PDBplk-1763533181437-0.0.1.1-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + X-Content-Type-Options: + - nosniff + Server: + - cloudflare + Cf-Ray: + - 9a0d98ec28132320-SJC + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: !binary |- + ewogICJpZCI6ICJjaGF0Y21wbC1DZFZwVWtBNWhFWDRVQzBYa2pvdUVQdTVLWkc3ViIsCiAgIm9iamVjdCI6ICJjaGF0LmNvbXBsZXRpb24iLAogICJjcmVhdGVkIjogMTc2MzUzMzE4MCwKICAibW9kZWwiOiAiZ3B0LTRvLW1pbmktMjAyNC0wNy0xOCIsCiAgImNob2ljZXMiOiBbCiAgICB7CiAgICAgICJpbmRleCI6IDAsCiAgICAgICJtZXNzYWdlIjogewogICAgICAgICJyb2xlIjogImFzc2lzdGFudCIsCiAgICAgICAgImNvbnRlbnQiOiAiVGhlIHdlYXRoZXIgaW4gQm9zdG9uLCBNQSBpcyBjdXJyZW50bHkgNzLCsEYgYW5kIHN1bm55LiIsCiAgICAgICAgInJlZnVzYWwiOiBudWxsLAogICAgICAgICJhbm5vdGF0aW9ucyI6IFtdCiAgICAgIH0sCiAgICAgICJsb2dwcm9icyI6IG51bGwsCiAgICAgICJmaW5pc2hfcmVhc29uIjogInN0b3AiCiAgICB9CiAgXSwKICAidXNhZ2UiOiB7CiAgICAicHJvbXB0X3Rva2VucyI6IDEwNywKICAgICJjb21wbGV0aW9uX3Rva2VucyI6IDE1LAogICAgInRvdGFsX3Rva2VucyI6IDEyMiwKICAgICJwcm9tcHRfdG9rZW5zX2RldGFpbHMiOiB7CiAgICAgICJjYWNoZWRfdG9rZW5zIjogMCwKICAgICAgImF1ZGlvX3Rva2VucyI6IDAKICAgIH0sCiAgICAiY29tcGxldGlvbl90b2tlbnNfZGV0YWlscyI6IHsKICAgICAgInJlYXNvbmluZ190b2tlbnMiOiAwLAogICAgICAiYXVkaW9fdG9rZW5zIjogMCwKICAgICAgImFjY2VwdGVkX3ByZWRpY3Rpb25fdG9rZW5zIjogMCwKICAgICAgInJlamVjdGVkX3ByZWRpY3Rpb25fdG9rZW5zIjogMAogICAgfQogIH0sCiAgInNlcnZpY2VfdGllciI6ICJkZWZhdWx0IiwKICAic3lzdGVtX2ZpbmdlcnByaW50IjogImZwXzU2MGFmNmU1NTkiCn0K + recorded_at: Wed, 19 Nov 2025 06:19:41 GMT +recorded_with: VCR 6.3.1 diff --git a/test/fixtures/vcr_cassettes/integration/open_ai/chat/common_format/tools_test/test_agent_common_format_multiple_tools.yml b/test/fixtures/vcr_cassettes/integration/open_ai/chat/common_format/tools_test/test_agent_common_format_multiple_tools.yml new file mode 100644 index 00000000..41472fd3 --- /dev/null +++ b/test/fixtures/vcr_cassettes/integration/open_ai/chat/common_format/tools_test/test_agent_common_format_multiple_tools.yml @@ -0,0 +1,275 @@ +--- +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 weather in NYC and what''s 5 plus 3?"}],"tools":[{"type":"function","function":{"name":"get_weather","description":"Get + the current weather","parameters":{"type":"object","properties":{"location":{"type":"string"}},"required":["location"]}}},{"type":"function","function":{"name":"calculate","description":"Perform + basic arithmetic","parameters":{"type":"object","properties":{"operation":{"type":"string","enum":["add","subtract","multiply","divide"]},"a":{"type":"number"},"b":{"type":"number"}},"required":["operation","a","b"]}}}]}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json + User-Agent: + - OpenAI::Client/Ruby 0.35.1 + Host: + - api.openai.com + X-Stainless-Arch: + - arm64 + X-Stainless-Lang: + - ruby + X-Stainless-Os: + - MacOS + X-Stainless-Package-Version: + - 0.35.1 + X-Stainless-Runtime: + - ruby + X-Stainless-Runtime-Version: + - 3.4.7 + Content-Type: + - application/json + Openai-Organization: + - ORGANIZATION_ID + Openai-Project: + - PROJECT_ID + Authorization: + - Bearer ACCESS_TOKEN + X-Stainless-Retry-Count: + - '0' + X-Stainless-Timeout: + - '600.0' + Content-Length: + - '606' + response: + status: + code: 200 + message: OK + headers: + Date: + - Wed, 19 Nov 2025 06:19:38 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Access-Control-Expose-Headers: + - X-Request-ID + Openai-Organization: + - ORGANIZATION_ID + Openai-Processing-Ms: + - '1499' + Openai-Project: + - PROJECT_ID + Openai-Version: + - '2020-10-01' + X-Envoy-Upstream-Service-Time: + - '1512' + X-Ratelimit-Limit-Requests: + - '30000' + X-Ratelimit-Limit-Tokens: + - '150000000' + X-Ratelimit-Remaining-Requests: + - '29999' + X-Ratelimit-Remaining-Tokens: + - '149999987' + X-Ratelimit-Reset-Requests: + - 2ms + X-Ratelimit-Reset-Tokens: + - 0s + X-Request-Id: + - req_8e3adfd22084496b9afcbd29f61bc837 + X-Openai-Proxy-Wasm: + - v0.1 + Cf-Cache-Status: + - DYNAMIC + Set-Cookie: + - __cf_bm=ZDhVnbKJgDJGn800jvM.PZfjfbvE4zCjNXZGqoZql84-1763533178-1.0.1.1-g64kkkW4sGyKlHu5am8LIMdR2x1LndffpxapeOT9iybq_tb6y2oowcUMTZAEPTDd.Ox_KEQvjwhI1ppJgwwQNZfDx1b70PkQYMsQx7nukgQ; + path=/; expires=Wed, 19-Nov-25 06:49:38 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=_VOjYb2ODB2mKFxo3BE.aVL7nLoVqYhgUeSSZgxM5c8-1763533178827-0.0.1.1-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + X-Content-Type-Options: + - nosniff + Server: + - cloudflare + Cf-Ray: + - 9a0d98d57d1a67d9-SJC + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: | + { + "id": "chatcmpl-CdVpRQKSNGceawDsCb1qXM4zU9XSc", + "object": "chat.completion", + "created": 1763533177, + "model": "gpt-4o-mini-2024-07-18", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": null, + "tool_calls": [ + { + "id": "call_i8WxtsPg3J1MGzu9r7ZPUulR", + "type": "function", + "function": { + "name": "get_weather", + "arguments": "{\"location\": \"New York City\"}" + } + }, + { + "id": "call_vZkmLcCQjrNAygM9N5BHRVFH", + "type": "function", + "function": { + "name": "calculate", + "arguments": "{\"operation\": \"add\", \"a\": 5, \"b\": 3}" + } + } + ], + "refusal": null, + "annotations": [] + }, + "logprobs": null, + "finish_reason": "tool_calls" + } + ], + "usage": { + "prompt_tokens": 93, + "completion_tokens": 53, + "total_tokens": 146, + "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_560af6e559" + } + recorded_at: Wed, 19 Nov 2025 06:19:38 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 weather in NYC and what''s 5 plus 3?"},{"role":"assistant","refusal":null,"annotations":[],"tool_calls":[{"id":"call_i8WxtsPg3J1MGzu9r7ZPUulR","function":{"arguments":"{\"location\": + \"New York City\"}","name":"get_weather"},"type":"function"},{"id":"call_vZkmLcCQjrNAygM9N5BHRVFH","function":{"arguments":"{\"operation\": + \"add\", \"a\": 5, \"b\": 3}","name":"calculate"},"type":"function"}]},{"role":"tool","content":"{\"location\":\"New + York City\",\"temperature\":\"72°F\",\"conditions\":\"sunny\"}","tool_call_id":"call_i8WxtsPg3J1MGzu9r7ZPUulR"},{"role":"tool","content":"{\"operation\":\"add\",\"a\":5,\"b\":3,\"result\":8}","tool_call_id":"call_vZkmLcCQjrNAygM9N5BHRVFH"}],"tools":[{"type":"function","function":{"name":"get_weather","description":"Get + the current weather","parameters":{"type":"object","properties":{"location":{"type":"string"}},"required":["location"]}}},{"type":"function","function":{"name":"calculate","description":"Perform + basic arithmetic","parameters":{"type":"object","properties":{"operation":{"type":"string","enum":["add","subtract","multiply","divide"]},"a":{"type":"number"},"b":{"type":"number"}},"required":["operation","a","b"]}}}]}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json + User-Agent: + - OpenAI::Client/Ruby 0.35.1 + Host: + - api.openai.com + X-Stainless-Arch: + - arm64 + X-Stainless-Lang: + - ruby + X-Stainless-Os: + - MacOS + X-Stainless-Package-Version: + - 0.35.1 + X-Stainless-Runtime: + - ruby + X-Stainless-Runtime-Version: + - 3.4.7 + Content-Type: + - application/json + Openai-Organization: + - ORGANIZATION_ID + Openai-Project: + - PROJECT_ID + Authorization: + - Bearer ACCESS_TOKEN + X-Stainless-Retry-Count: + - '0' + X-Stainless-Timeout: + - '600.0' + Content-Length: + - '1248' + response: + status: + code: 200 + message: OK + headers: + Date: + - Wed, 19 Nov 2025 06:19:40 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Access-Control-Expose-Headers: + - X-Request-ID + Openai-Organization: + - ORGANIZATION_ID + Openai-Processing-Ms: + - '963' + Openai-Project: + - PROJECT_ID + Openai-Version: + - '2020-10-01' + X-Envoy-Upstream-Service-Time: + - '975' + X-Ratelimit-Limit-Requests: + - '30000' + X-Ratelimit-Limit-Tokens: + - '150000000' + X-Ratelimit-Remaining-Requests: + - '29999' + X-Ratelimit-Remaining-Tokens: + - '149999955' + X-Ratelimit-Reset-Requests: + - 2ms + X-Ratelimit-Reset-Tokens: + - 0s + X-Request-Id: + - req_ad2f5b53733c4bffb7eb64dba54f6ade + X-Openai-Proxy-Wasm: + - v0.1 + Cf-Cache-Status: + - DYNAMIC + Set-Cookie: + - __cf_bm=SuqXPX0K1iEA_4kiC7NSWn2obgTaFu6C3xI4Ug0FFRg-1763533180-1.0.1.1-nrKZPVxQATV5tEDwTqyABFY9gjqS4Ns34vjr5vNuP69GixyjRGgs1qQmjol0CnC6lqTTTkmVCAXeXRmNrKW.5s.mbY7VDwYK0kkVGfIHmOA; + path=/; expires=Wed, 19-Nov-25 06:49:40 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=qDdGdsF64Zlc5NBKVmxMFFknA8SeaLwB5I3mBV8R.Mg-1763533180022-0.0.1.1-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + X-Content-Type-Options: + - nosniff + Server: + - cloudflare + Cf-Ray: + - 9a0d98e07ed9251d-SJC + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: !binary |- + ewogICJpZCI6ICJjaGF0Y21wbC1DZFZwVDFzcldwcDY2T2IwN3VvckpXZm9xMURuciIsCiAgIm9iamVjdCI6ICJjaGF0LmNvbXBsZXRpb24iLAogICJjcmVhdGVkIjogMTc2MzUzMzE3OSwKICAibW9kZWwiOiAiZ3B0LTRvLW1pbmktMjAyNC0wNy0xOCIsCiAgImNob2ljZXMiOiBbCiAgICB7CiAgICAgICJpbmRleCI6IDAsCiAgICAgICJtZXNzYWdlIjogewogICAgICAgICJyb2xlIjogImFzc2lzdGFudCIsCiAgICAgICAgImNvbnRlbnQiOiAiVGhlIHdlYXRoZXIgaW4gTmV3IFlvcmsgQ2l0eSBpcyBzdW5ueSB3aXRoIGEgdGVtcGVyYXR1cmUgb2YgNzLCsEYuIEFkZGl0aW9uYWxseSwgNSBwbHVzIDMgZXF1YWxzIDguIiwKICAgICAgICAicmVmdXNhbCI6IG51bGwsCiAgICAgICAgImFubm90YXRpb25zIjogW10KICAgICAgfSwKICAgICAgImxvZ3Byb2JzIjogbnVsbCwKICAgICAgImZpbmlzaF9yZWFzb24iOiAic3RvcCIKICAgIH0KICBdLAogICJ1c2FnZSI6IHsKICAgICJwcm9tcHRfdG9rZW5zIjogMjA3LAogICAgImNvbXBsZXRpb25fdG9rZW5zIjogMjgsCiAgICAidG90YWxfdG9rZW5zIjogMjM1LAogICAgInByb21wdF90b2tlbnNfZGV0YWlscyI6IHsKICAgICAgImNhY2hlZF90b2tlbnMiOiAwLAogICAgICAiYXVkaW9fdG9rZW5zIjogMAogICAgfSwKICAgICJjb21wbGV0aW9uX3Rva2Vuc19kZXRhaWxzIjogewogICAgICAicmVhc29uaW5nX3Rva2VucyI6IDAsCiAgICAgICJhdWRpb190b2tlbnMiOiAwLAogICAgICAiYWNjZXB0ZWRfcHJlZGljdGlvbl90b2tlbnMiOiAwLAogICAgICAicmVqZWN0ZWRfcHJlZGljdGlvbl90b2tlbnMiOiAwCiAgICB9CiAgfSwKICAic2VydmljZV90aWVyIjogImRlZmF1bHQiLAogICJzeXN0ZW1fZmluZ2VycHJpbnQiOiAiZnBfNTYwYWY2ZTU1OSIKfQo= + recorded_at: Wed, 19 Nov 2025 06:19:40 GMT +recorded_with: VCR 6.3.1 diff --git a/test/fixtures/vcr_cassettes/integration/open_ai/chat/common_format/tools_test/test_agent_common_format_parameters.yml b/test/fixtures/vcr_cassettes/integration/open_ai/chat/common_format/tools_test/test_agent_common_format_parameters.yml new file mode 100644 index 00000000..d5978684 --- /dev/null +++ b/test/fixtures/vcr_cassettes/integration/open_ai/chat/common_format/tools_test/test_agent_common_format_parameters.yml @@ -0,0 +1,266 @@ +--- +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 weather in San Francisco?"}],"tools":[{"type":"function","function":{"name":"get_weather","description":"Get + the current weather in a given location","parameters":{"type":"object","properties":{"location":{"type":"string","description":"The + city and state, e.g. San Francisco, CA"}},"required":["location"]}}}]}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json + User-Agent: + - OpenAI::Client/Ruby 0.35.1 + Host: + - api.openai.com + X-Stainless-Arch: + - arm64 + X-Stainless-Lang: + - ruby + X-Stainless-Os: + - MacOS + X-Stainless-Package-Version: + - 0.35.1 + X-Stainless-Runtime: + - ruby + X-Stainless-Runtime-Version: + - 3.4.7 + Content-Type: + - application/json + Openai-Organization: + - ORGANIZATION_ID + Openai-Project: + - PROJECT_ID + Authorization: + - Bearer ACCESS_TOKEN + X-Stainless-Retry-Count: + - '0' + X-Stainless-Timeout: + - '600.0' + Content-Length: + - '383' + response: + status: + code: 200 + message: OK + headers: + Date: + - Wed, 19 Nov 2025 06:19:43 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Access-Control-Expose-Headers: + - X-Request-ID + Openai-Organization: + - ORGANIZATION_ID + Openai-Processing-Ms: + - '719' + Openai-Project: + - PROJECT_ID + Openai-Version: + - '2020-10-01' + X-Envoy-Upstream-Service-Time: + - '1013' + X-Ratelimit-Limit-Requests: + - '30000' + X-Ratelimit-Limit-Tokens: + - '150000000' + X-Ratelimit-Remaining-Requests: + - '29999' + X-Ratelimit-Remaining-Tokens: + - '149999987' + X-Ratelimit-Reset-Requests: + - 2ms + X-Ratelimit-Reset-Tokens: + - 0s + X-Request-Id: + - req_9c76bfcee9e24db6ba2125251b9bbe09 + X-Openai-Proxy-Wasm: + - v0.1 + Cf-Cache-Status: + - DYNAMIC + Set-Cookie: + - __cf_bm=zHHfyU7QyWdEXM1r55P0QO.cCLTac4iQqUbRb2pG2gg-1763533183-1.0.1.1-7KKtxBMbDzMV9GIX3HDKiexgHoejU7ulHLiC7yTFQLXO_U70RQXbQ6nTK34kfaqP5qEzXWtvHilgveB9lRtlDf2pQcnFEx0bWQJ3pCnJMsg; + path=/; expires=Wed, 19-Nov-25 06:49:43 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=owN32mUuz6Fwi4TLjUfadE5l4Owuihm8AFUsdhcP86A-1763533183591-0.0.1.1-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + X-Content-Type-Options: + - nosniff + Server: + - cloudflare + Cf-Ray: + - 9a0d98f0b91315d2-SJC + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: | + { + "id": "chatcmpl-CdVpWynp5v1j6c8ipsAX7bga0CL38", + "object": "chat.completion", + "created": 1763533182, + "model": "gpt-4o-mini-2024-07-18", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": null, + "tool_calls": [ + { + "id": "call_aHSN78tOGn5Job0vM895ZA48", + "type": "function", + "function": { + "name": "get_weather", + "arguments": "{\"location\":\"San Francisco, CA\"}" + } + } + ], + "refusal": null, + "annotations": [] + }, + "logprobs": null, + "finish_reason": "tool_calls" + } + ], + "usage": { + "prompt_tokens": 67, + "completion_tokens": 17, + "total_tokens": 84, + "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_560af6e559" + } + recorded_at: Wed, 19 Nov 2025 06:19:43 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 weather in San Francisco?"},{"role":"assistant","refusal":null,"annotations":[],"tool_calls":[{"id":"call_aHSN78tOGn5Job0vM895ZA48","function":{"arguments":"{\"location\":\"San + Francisco, CA\"}","name":"get_weather"},"type":"function"}]},{"role":"tool","content":"{\"location\":\"San + Francisco, CA\",\"temperature\":\"72°F\",\"conditions\":\"sunny\"}","tool_call_id":"call_aHSN78tOGn5Job0vM895ZA48"}],"tools":[{"type":"function","function":{"name":"get_weather","description":"Get + the current weather in a given location","parameters":{"type":"object","properties":{"location":{"type":"string","description":"The + city and state, e.g. San Francisco, CA"}},"required":["location"]}}}]}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json + User-Agent: + - OpenAI::Client/Ruby 0.35.1 + Host: + - api.openai.com + X-Stainless-Arch: + - arm64 + X-Stainless-Lang: + - ruby + X-Stainless-Os: + - MacOS + X-Stainless-Package-Version: + - 0.35.1 + X-Stainless-Runtime: + - ruby + X-Stainless-Runtime-Version: + - 3.4.7 + Content-Type: + - application/json + Openai-Organization: + - ORGANIZATION_ID + Openai-Project: + - PROJECT_ID + Authorization: + - Bearer ACCESS_TOKEN + X-Stainless-Retry-Count: + - '0' + X-Stainless-Timeout: + - '600.0' + Content-Length: + - '756' + response: + status: + code: 200 + message: OK + headers: + Date: + - Wed, 19 Nov 2025 06:19:44 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Access-Control-Expose-Headers: + - X-Request-ID + Openai-Organization: + - ORGANIZATION_ID + Openai-Processing-Ms: + - '957' + Openai-Project: + - PROJECT_ID + Openai-Version: + - '2020-10-01' + X-Envoy-Upstream-Service-Time: + - '970' + X-Ratelimit-Limit-Requests: + - '30000' + X-Ratelimit-Limit-Tokens: + - '150000000' + X-Ratelimit-Remaining-Requests: + - '29999' + X-Ratelimit-Remaining-Tokens: + - '149999967' + X-Ratelimit-Reset-Requests: + - 2ms + X-Ratelimit-Reset-Tokens: + - 0s + X-Request-Id: + - req_e5e91f6542cb401fbd39d17fd3925b82 + X-Openai-Proxy-Wasm: + - v0.1 + Cf-Cache-Status: + - DYNAMIC + Set-Cookie: + - __cf_bm=C614RHk1epWhBNfPqCE_qW3TSyaop_2SSWJj3XKuZbc-1763533184-1.0.1.1-IVd5Tx8PIBhJT7zyPrVK93cXg5SHB2v2QF5ZqaVqazj5n1t3DknF1JFalXKq2egYJF2ei_mNQVeu9zytpKgDQQmQ4sOVzCwA8ZnLnRVNsV0; + path=/; expires=Wed, 19-Nov-25 06:49:44 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=3FbJap_puCS_vLUo2Ae8adAY.ZeEbul47TeohdzhfCc-1763533184733-0.0.1.1-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + X-Content-Type-Options: + - nosniff + Server: + - cloudflare + Cf-Ray: + - 9a0d98fddb349465-SJC + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: !binary |- + ewogICJpZCI6ICJjaGF0Y21wbC1DZFZwWGlkRGlKcUU4S3NWeFFyWlFFQWpVR2dwQyIsCiAgIm9iamVjdCI6ICJjaGF0LmNvbXBsZXRpb24iLAogICJjcmVhdGVkIjogMTc2MzUzMzE4MywKICAibW9kZWwiOiAiZ3B0LTRvLW1pbmktMjAyNC0wNy0xOCIsCiAgImNob2ljZXMiOiBbCiAgICB7CiAgICAgICJpbmRleCI6IDAsCiAgICAgICJtZXNzYWdlIjogewogICAgICAgICJyb2xlIjogImFzc2lzdGFudCIsCiAgICAgICAgImNvbnRlbnQiOiAiVGhlIHdlYXRoZXIgaW4gU2FuIEZyYW5jaXNjbywgQ0EgaXMgY3VycmVudGx5IDcywrBGIGFuZCBzdW5ueS4iLAogICAgICAgICJyZWZ1c2FsIjogbnVsbCwKICAgICAgICAiYW5ub3RhdGlvbnMiOiBbXQogICAgICB9LAogICAgICAibG9ncHJvYnMiOiBudWxsLAogICAgICAiZmluaXNoX3JlYXNvbiI6ICJzdG9wIgogICAgfQogIF0sCiAgInVzYWdlIjogewogICAgInByb21wdF90b2tlbnMiOiAxMTAsCiAgICAiY29tcGxldGlvbl90b2tlbnMiOiAxNiwKICAgICJ0b3RhbF90b2tlbnMiOiAxMjYsCiAgICAicHJvbXB0X3Rva2Vuc19kZXRhaWxzIjogewogICAgICAiY2FjaGVkX3Rva2VucyI6IDAsCiAgICAgICJhdWRpb190b2tlbnMiOiAwCiAgICB9LAogICAgImNvbXBsZXRpb25fdG9rZW5zX2RldGFpbHMiOiB7CiAgICAgICJyZWFzb25pbmdfdG9rZW5zIjogMCwKICAgICAgImF1ZGlvX3Rva2VucyI6IDAsCiAgICAgICJhY2NlcHRlZF9wcmVkaWN0aW9uX3Rva2VucyI6IDAsCiAgICAgICJyZWplY3RlZF9wcmVkaWN0aW9uX3Rva2VucyI6IDAKICAgIH0KICB9LAogICJzZXJ2aWNlX3RpZXIiOiAiZGVmYXVsdCIsCiAgInN5c3RlbV9maW5nZXJwcmludCI6ICJmcF81NjBhZjZlNTU5Igp9Cg== + recorded_at: Wed, 19 Nov 2025 06:19:44 GMT +recorded_with: VCR 6.3.1 diff --git a/test/fixtures/vcr_cassettes/integration/open_ai/chat/common_format/tools_test/test_agent_common_format_tool_choice_auto.yml b/test/fixtures/vcr_cassettes/integration/open_ai/chat/common_format/tools_test/test_agent_common_format_tool_choice_auto.yml new file mode 100644 index 00000000..894ac160 --- /dev/null +++ b/test/fixtures/vcr_cassettes/integration/open_ai/chat/common_format/tools_test/test_agent_common_format_tool_choice_auto.yml @@ -0,0 +1,145 @@ +--- +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 weather?"}],"tools":[{"type":"function","function":{"name":"get_weather","description":"Get + weather","parameters":{"type":"object","properties":{"location":{"type":"string"}},"required":["location"]}}}],"tool_choice":"auto"}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json + User-Agent: + - OpenAI::Client/Ruby 0.35.1 + Host: + - api.openai.com + X-Stainless-Arch: + - arm64 + X-Stainless-Lang: + - ruby + X-Stainless-Os: + - MacOS + X-Stainless-Package-Version: + - 0.35.1 + X-Stainless-Runtime: + - ruby + X-Stainless-Runtime-Version: + - 3.4.7 + Content-Type: + - application/json + Openai-Organization: + - ORGANIZATION_ID + Openai-Project: + - PROJECT_ID + Authorization: + - Bearer ACCESS_TOKEN + X-Stainless-Retry-Count: + - '0' + X-Stainless-Timeout: + - '600.0' + Content-Length: + - '296' + response: + status: + code: 200 + message: OK + headers: + Date: + - Wed, 19 Nov 2025 06:19:35 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Access-Control-Expose-Headers: + - X-Request-ID + Openai-Organization: + - ORGANIZATION_ID + Openai-Processing-Ms: + - '670' + Openai-Project: + - PROJECT_ID + Openai-Version: + - '2020-10-01' + X-Envoy-Upstream-Service-Time: + - '685' + X-Ratelimit-Limit-Requests: + - '30000' + X-Ratelimit-Limit-Tokens: + - '150000000' + X-Ratelimit-Remaining-Requests: + - '29999' + X-Ratelimit-Remaining-Tokens: + - '149999992' + X-Ratelimit-Reset-Requests: + - 2ms + X-Ratelimit-Reset-Tokens: + - 0s + X-Request-Id: + - req_978be8700a244f8ba1c9bcde3c50c1e8 + X-Openai-Proxy-Wasm: + - v0.1 + Cf-Cache-Status: + - DYNAMIC + Set-Cookie: + - __cf_bm=cNPTR03lR9YR_4D2gPDCCkNz3h1jHPyANvDH8ppQAOI-1763533175-1.0.1.1-1uGYIBRpbAxBs_x._dEhYzH4XzKUMtcfhVEYceAn47pSyMCTcRM0n02fupc45esvGNOWXtpk1BYai14mdSxMSzzzR_4GVzlanYDW.v5xweE; + path=/; expires=Wed, 19-Nov-25 06:49:35 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=sDnq4PWVIUj_YHakkCIyHieV6iSN3Lhn8fGZeHfAmBI-1763533175695-0.0.1.1-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + X-Content-Type-Options: + - nosniff + Server: + - cloudflare + Cf-Ray: + - 9a0d98c738db67cd-SJC + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: | + { + "id": "chatcmpl-CdVpPg42Ew60O5JjNzLghk6TyM6EU", + "object": "chat.completion", + "created": 1763533175, + "model": "gpt-4o-mini-2024-07-18", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": "Could you please provide me with the location for which you would like to know the weather?", + "refusal": null, + "annotations": [] + }, + "logprobs": null, + "finish_reason": "stop" + } + ], + "usage": { + "prompt_tokens": 44, + "completion_tokens": 19, + "total_tokens": 63, + "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_560af6e559" + } + recorded_at: Wed, 19 Nov 2025 06:19:35 GMT +recorded_with: VCR 6.3.1 diff --git a/test/fixtures/vcr_cassettes/integration/open_ai/chat/common_format/tools_test/test_agent_common_format_tool_choice_required.yml b/test/fixtures/vcr_cassettes/integration/open_ai/chat/common_format/tools_test/test_agent_common_format_tool_choice_required.yml new file mode 100644 index 00000000..adde4c2f --- /dev/null +++ b/test/fixtures/vcr_cassettes/integration/open_ai/chat/common_format/tools_test/test_agent_common_format_tool_choice_required.yml @@ -0,0 +1,264 @@ +--- +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 weather?"}],"tools":[{"type":"function","function":{"name":"get_weather","description":"Get + weather","parameters":{"type":"object","properties":{"location":{"type":"string"}},"required":["location"]}}}],"tool_choice":"required"}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json + User-Agent: + - OpenAI::Client/Ruby 0.35.1 + Host: + - api.openai.com + X-Stainless-Arch: + - arm64 + X-Stainless-Lang: + - ruby + X-Stainless-Os: + - MacOS + X-Stainless-Package-Version: + - 0.35.1 + X-Stainless-Runtime: + - ruby + X-Stainless-Runtime-Version: + - 3.4.7 + Content-Type: + - application/json + Openai-Organization: + - ORGANIZATION_ID + Openai-Project: + - PROJECT_ID + Authorization: + - Bearer ACCESS_TOKEN + X-Stainless-Retry-Count: + - '0' + X-Stainless-Timeout: + - '600.0' + Content-Length: + - '300' + response: + status: + code: 200 + message: OK + headers: + Date: + - Wed, 19 Nov 2025 06:19:36 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Access-Control-Expose-Headers: + - X-Request-ID + Openai-Organization: + - ORGANIZATION_ID + Openai-Processing-Ms: + - '596' + Openai-Project: + - PROJECT_ID + Openai-Version: + - '2020-10-01' + X-Envoy-Upstream-Service-Time: + - '613' + X-Ratelimit-Limit-Requests: + - '30000' + X-Ratelimit-Limit-Tokens: + - '150000000' + X-Ratelimit-Remaining-Requests: + - '29999' + X-Ratelimit-Remaining-Tokens: + - '149999992' + X-Ratelimit-Reset-Requests: + - 2ms + X-Ratelimit-Reset-Tokens: + - 0s + X-Request-Id: + - req_2c9edccf0e9741b987a7d8cd07144aa6 + X-Openai-Proxy-Wasm: + - v0.1 + Cf-Cache-Status: + - DYNAMIC + Set-Cookie: + - __cf_bm=ElmZbT2aJvTXMXwlnGm767QREKqjbYhSivaWflTxDYg-1763533176-1.0.1.1-LQpebpjTT96aDTdmeILrUD3ROY7ZkVlQx5.ta0AlvBNzu4.1bN5Jw95dqjoS0kwgGM2TAKcJ3i9bQi3fGn26Kq73Ga_sOCP7QdJ_kdZty5s; + path=/; expires=Wed, 19-Nov-25 06:49:36 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=Szz5KpMMvuAVbiXch_IFwrlTVWORSkFf0PNZaaKbXXU-1763533176505-0.0.1.1-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + X-Content-Type-Options: + - nosniff + Server: + - cloudflare + Cf-Ray: + - 9a0d98cccde1ee17-SJC + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: | + { + "id": "chatcmpl-CdVpPAZdaDib1KVsiM2evVA0OzEoR", + "object": "chat.completion", + "created": 1763533175, + "model": "gpt-4o-mini-2024-07-18", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": null, + "tool_calls": [ + { + "id": "call_bzruxle33QGVUqAfXPmbniXc", + "type": "function", + "function": { + "name": "get_weather", + "arguments": "{\"location\":\"current location\"}" + } + } + ], + "refusal": null, + "annotations": [] + }, + "logprobs": null, + "finish_reason": "tool_calls" + } + ], + "usage": { + "prompt_tokens": 44, + "completion_tokens": 15, + "total_tokens": 59, + "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_560af6e559" + } + recorded_at: Wed, 19 Nov 2025 06:19:36 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 weather?"},{"role":"assistant","refusal":null,"annotations":[],"tool_calls":[{"id":"call_bzruxle33QGVUqAfXPmbniXc","function":{"arguments":"{\"location\":\"current + location\"}","name":"get_weather"},"type":"function"}]},{"role":"tool","content":"{\"location\":\"current + location\",\"temperature\":\"72°F\",\"conditions\":\"sunny\"}","tool_call_id":"call_bzruxle33QGVUqAfXPmbniXc"}],"tools":[{"type":"function","function":{"name":"get_weather","description":"Get + weather","parameters":{"type":"object","properties":{"location":{"type":"string"}},"required":["location"]}}}],"tool_choice":null}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json + User-Agent: + - OpenAI::Client/Ruby 0.35.1 + Host: + - api.openai.com + X-Stainless-Arch: + - arm64 + X-Stainless-Lang: + - ruby + X-Stainless-Os: + - MacOS + X-Stainless-Package-Version: + - 0.35.1 + X-Stainless-Runtime: + - ruby + X-Stainless-Runtime-Version: + - 3.4.7 + Content-Type: + - application/json + Openai-Organization: + - ORGANIZATION_ID + Openai-Project: + - PROJECT_ID + Authorization: + - Bearer ACCESS_TOKEN + X-Stainless-Retry-Count: + - '0' + X-Stainless-Timeout: + - '600.0' + Content-Length: + - '665' + response: + status: + code: 200 + message: OK + headers: + Date: + - Wed, 19 Nov 2025 06:19:37 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Access-Control-Expose-Headers: + - X-Request-ID + Openai-Organization: + - ORGANIZATION_ID + Openai-Processing-Ms: + - '424' + Openai-Project: + - PROJECT_ID + Openai-Version: + - '2020-10-01' + X-Envoy-Upstream-Service-Time: + - '439' + X-Ratelimit-Limit-Requests: + - '30000' + X-Ratelimit-Limit-Tokens: + - '150000000' + X-Ratelimit-Remaining-Requests: + - '29999' + X-Ratelimit-Remaining-Tokens: + - '149999972' + X-Ratelimit-Reset-Requests: + - 2ms + X-Ratelimit-Reset-Tokens: + - 0s + X-Request-Id: + - req_b9a8b734e2e34964b449ace9a97b1f5e + X-Openai-Proxy-Wasm: + - v0.1 + Cf-Cache-Status: + - DYNAMIC + Set-Cookie: + - __cf_bm=1VVGBZBPEXN4yQliDPNlBGb7FyeNdMAjhjjKHhULHOU-1763533177-1.0.1.1-XhdVBiYdx5UWPXnHzAXrIEa9ih7IEhmX23lhz8duzfeSb9_90E4VXkXPMDt8sKbiDR2ObLA1xVKvUOLwUWCog_3bhP7u6hF0vCznVpqYfyk; + path=/; expires=Wed, 19-Nov-25 06:49:37 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=tGUhZAM8i5Le9iUwmdV1_8dil0fvRL2xgYnm9hwN268-1763533177130-0.0.1.1-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + X-Content-Type-Options: + - nosniff + Server: + - cloudflare + Cf-Ray: + - 9a0d98d1c974dc0d-SJC + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: !binary |- + ewogICJpZCI6ICJjaGF0Y21wbC1DZFZwUVRCTFhNNzNPNkwxMWlLQUhaMDVJVEVxSCIsCiAgIm9iamVjdCI6ICJjaGF0LmNvbXBsZXRpb24iLAogICJjcmVhdGVkIjogMTc2MzUzMzE3NiwKICAibW9kZWwiOiAiZ3B0LTRvLW1pbmktMjAyNC0wNy0xOCIsCiAgImNob2ljZXMiOiBbCiAgICB7CiAgICAgICJpbmRleCI6IDAsCiAgICAgICJtZXNzYWdlIjogewogICAgICAgICJyb2xlIjogImFzc2lzdGFudCIsCiAgICAgICAgImNvbnRlbnQiOiAiVGhlIGN1cnJlbnQgd2VhdGhlciBpcyA3MsKwRiBhbmQgc3VubnkuIiwKICAgICAgICAicmVmdXNhbCI6IG51bGwsCiAgICAgICAgImFubm90YXRpb25zIjogW10KICAgICAgfSwKICAgICAgImxvZ3Byb2JzIjogbnVsbCwKICAgICAgImZpbmlzaF9yZWFzb24iOiAic3RvcCIKICAgIH0KICBdLAogICJ1c2FnZSI6IHsKICAgICJwcm9tcHRfdG9rZW5zIjogODMsCiAgICAiY29tcGxldGlvbl90b2tlbnMiOiAxMSwKICAgICJ0b3RhbF90b2tlbnMiOiA5NCwKICAgICJwcm9tcHRfdG9rZW5zX2RldGFpbHMiOiB7CiAgICAgICJjYWNoZWRfdG9rZW5zIjogMCwKICAgICAgImF1ZGlvX3Rva2VucyI6IDAKICAgIH0sCiAgICAiY29tcGxldGlvbl90b2tlbnNfZGV0YWlscyI6IHsKICAgICAgInJlYXNvbmluZ190b2tlbnMiOiAwLAogICAgICAiYXVkaW9fdG9rZW5zIjogMCwKICAgICAgImFjY2VwdGVkX3ByZWRpY3Rpb25fdG9rZW5zIjogMCwKICAgICAgInJlamVjdGVkX3ByZWRpY3Rpb25fdG9rZW5zIjogMAogICAgfQogIH0sCiAgInNlcnZpY2VfdGllciI6ICJkZWZhdWx0IiwKICAic3lzdGVtX2ZpbmdlcnByaW50IjogImZwXzU2MGFmNmU1NTkiCn0K + recorded_at: Wed, 19 Nov 2025 06:19:37 GMT +recorded_with: VCR 6.3.1 diff --git a/test/fixtures/vcr_cassettes/integration/open_ai/chat/common_format/tools_test/test_agent_common_format_tool_choice_specific.yml b/test/fixtures/vcr_cassettes/integration/open_ai/chat/common_format/tools_test/test_agent_common_format_tool_choice_specific.yml new file mode 100644 index 00000000..bf75bae9 --- /dev/null +++ b/test/fixtures/vcr_cassettes/integration/open_ai/chat/common_format/tools_test/test_agent_common_format_tool_choice_specific.yml @@ -0,0 +1,264 @@ +--- +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 weather?"}],"tools":[{"type":"function","function":{"name":"get_weather","description":"Get + weather","parameters":{"type":"object","properties":{"location":{"type":"string"}},"required":["location"]}}}],"tool_choice":{"type":"function","function":{"name":"get_weather"}}}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json + User-Agent: + - OpenAI::Client/Ruby 0.35.1 + Host: + - api.openai.com + X-Stainless-Arch: + - arm64 + X-Stainless-Lang: + - ruby + X-Stainless-Os: + - MacOS + X-Stainless-Package-Version: + - 0.35.1 + X-Stainless-Runtime: + - ruby + X-Stainless-Runtime-Version: + - 3.4.7 + Content-Type: + - application/json + Openai-Organization: + - ORGANIZATION_ID + Openai-Project: + - PROJECT_ID + Authorization: + - Bearer ACCESS_TOKEN + X-Stainless-Retry-Count: + - '0' + X-Stainless-Timeout: + - '600.0' + Content-Length: + - '343' + response: + status: + code: 200 + message: OK + headers: + Date: + - Wed, 19 Nov 2025 06:19:45 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Access-Control-Expose-Headers: + - X-Request-ID + Openai-Organization: + - ORGANIZATION_ID + Openai-Processing-Ms: + - '548' + Openai-Project: + - PROJECT_ID + Openai-Version: + - '2020-10-01' + X-Envoy-Upstream-Service-Time: + - '571' + X-Ratelimit-Limit-Requests: + - '30000' + X-Ratelimit-Limit-Tokens: + - '150000000' + X-Ratelimit-Remaining-Requests: + - '29999' + X-Ratelimit-Remaining-Tokens: + - '149999992' + X-Ratelimit-Reset-Requests: + - 2ms + X-Ratelimit-Reset-Tokens: + - 0s + X-Request-Id: + - req_e0ace4243c3941b29e2fb1372df37315 + X-Openai-Proxy-Wasm: + - v0.1 + Cf-Cache-Status: + - DYNAMIC + Set-Cookie: + - __cf_bm=n0dq6T0ebKEtYnIjOWbuy_rezLllJH0SEpv7TmFVqF8-1763533185-1.0.1.1-4TP3jAbGQrO5bEnDMcERuYaPCcevfEJHv6KyuxmBNuuXb094NQHPS8PGchA7swyVhgpcsxPXdRuCgtH3q0gnBSFb_.8oP9X8asbG3TBYzwo; + path=/; expires=Wed, 19-Nov-25 06:49:45 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=6CBvHysJu4rHoKQxg0KRHBd9Dn29._.dC._4oMAq5GQ-1763533185472-0.0.1.1-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + X-Content-Type-Options: + - nosniff + Server: + - cloudflare + Cf-Ray: + - 9a0d99050e0f159a-SJC + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: | + { + "id": "chatcmpl-CdVpY70udpeZfQDpZ7mIZyjG1FlwH", + "object": "chat.completion", + "created": 1763533184, + "model": "gpt-4o-mini-2024-07-18", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": null, + "tool_calls": [ + { + "id": "call_ygHSYPgcYpF3inglYk2Cem6r", + "type": "function", + "function": { + "name": "get_weather", + "arguments": "{\"location\":\"New York\"}" + } + } + ], + "refusal": null, + "annotations": [] + }, + "logprobs": null, + "finish_reason": "stop" + } + ], + "usage": { + "prompt_tokens": 53, + "completion_tokens": 6, + "total_tokens": 59, + "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_560af6e559" + } + recorded_at: Wed, 19 Nov 2025 06:19:45 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 weather?"},{"role":"assistant","refusal":null,"annotations":[],"tool_calls":[{"id":"call_ygHSYPgcYpF3inglYk2Cem6r","function":{"arguments":"{\"location\":\"New + York\"}","name":"get_weather"},"type":"function"}]},{"role":"tool","content":"{\"location\":\"New + York\",\"temperature\":\"72°F\",\"conditions\":\"sunny\"}","tool_call_id":"call_ygHSYPgcYpF3inglYk2Cem6r"}],"tools":[{"type":"function","function":{"name":"get_weather","description":"Get + weather","parameters":{"type":"object","properties":{"location":{"type":"string"}},"required":["location"]}}}],"tool_choice":null}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json + User-Agent: + - OpenAI::Client/Ruby 0.35.1 + Host: + - api.openai.com + X-Stainless-Arch: + - arm64 + X-Stainless-Lang: + - ruby + X-Stainless-Os: + - MacOS + X-Stainless-Package-Version: + - 0.35.1 + X-Stainless-Runtime: + - ruby + X-Stainless-Runtime-Version: + - 3.4.7 + Content-Type: + - application/json + Openai-Organization: + - ORGANIZATION_ID + Openai-Project: + - PROJECT_ID + Authorization: + - Bearer ACCESS_TOKEN + X-Stainless-Retry-Count: + - '0' + X-Stainless-Timeout: + - '600.0' + Content-Length: + - '649' + response: + status: + code: 200 + message: OK + headers: + Date: + - Wed, 19 Nov 2025 06:19:46 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Access-Control-Expose-Headers: + - X-Request-ID + Openai-Organization: + - ORGANIZATION_ID + Openai-Processing-Ms: + - '517' + Openai-Project: + - PROJECT_ID + Openai-Version: + - '2020-10-01' + X-Envoy-Upstream-Service-Time: + - '530' + X-Ratelimit-Limit-Requests: + - '30000' + X-Ratelimit-Limit-Tokens: + - '150000000' + X-Ratelimit-Remaining-Requests: + - '29999' + X-Ratelimit-Remaining-Tokens: + - '149999975' + X-Ratelimit-Reset-Requests: + - 2ms + X-Ratelimit-Reset-Tokens: + - 0s + X-Request-Id: + - req_716f82849f684aad8d6b8ab2fa8cc62a + X-Openai-Proxy-Wasm: + - v0.1 + Cf-Cache-Status: + - DYNAMIC + Set-Cookie: + - __cf_bm=xr7j0ft6BfSCBCwIfw5ZuMgpQvWN0fZe2UDLz8d6QDM-1763533186-1.0.1.1-lgBrXdE45SDPyqcGHN_UUYKO05GxwuGyYQM9nv8uv0b5l9l2icuESpLFpziwcfVDjxW1mOZ680qzGL57COT0nkudiIk8UF9fvSz_8YRWWXk; + path=/; expires=Wed, 19-Nov-25 06:49:46 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=177kKEkHd9lgHO6SeMzv8_YR2aQAILRhC7MIeU8k3Jo-1763533186126-0.0.1.1-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + X-Content-Type-Options: + - nosniff + Server: + - cloudflare + Cf-Ray: + - 9a0d99099d64efbd-SJC + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: !binary |- + ewogICJpZCI6ICJjaGF0Y21wbC1DZFZwWjBiVkZUV3pzZFdKeFdjU25FWkVSWXlFSiIsCiAgIm9iamVjdCI6ICJjaGF0LmNvbXBsZXRpb24iLAogICJjcmVhdGVkIjogMTc2MzUzMzE4NSwKICAibW9kZWwiOiAiZ3B0LTRvLW1pbmktMjAyNC0wNy0xOCIsCiAgImNob2ljZXMiOiBbCiAgICB7CiAgICAgICJpbmRleCI6IDAsCiAgICAgICJtZXNzYWdlIjogewogICAgICAgICJyb2xlIjogImFzc2lzdGFudCIsCiAgICAgICAgImNvbnRlbnQiOiAiVGhlIHdlYXRoZXIgaW4gTmV3IFlvcmsgaXMgY3VycmVudGx5IDcywrBGIGFuZCBzdW5ueS4iLAogICAgICAgICJyZWZ1c2FsIjogbnVsbCwKICAgICAgICAiYW5ub3RhdGlvbnMiOiBbXQogICAgICB9LAogICAgICAibG9ncHJvYnMiOiBudWxsLAogICAgICAiZmluaXNoX3JlYXNvbiI6ICJzdG9wIgogICAgfQogIF0sCiAgInVzYWdlIjogewogICAgInByb21wdF90b2tlbnMiOiA4MywKICAgICJjb21wbGV0aW9uX3Rva2VucyI6IDE0LAogICAgInRvdGFsX3Rva2VucyI6IDk3LAogICAgInByb21wdF90b2tlbnNfZGV0YWlscyI6IHsKICAgICAgImNhY2hlZF90b2tlbnMiOiAwLAogICAgICAiYXVkaW9fdG9rZW5zIjogMAogICAgfSwKICAgICJjb21wbGV0aW9uX3Rva2Vuc19kZXRhaWxzIjogewogICAgICAicmVhc29uaW5nX3Rva2VucyI6IDAsCiAgICAgICJhdWRpb190b2tlbnMiOiAwLAogICAgICAiYWNjZXB0ZWRfcHJlZGljdGlvbl90b2tlbnMiOiAwLAogICAgICAicmVqZWN0ZWRfcHJlZGljdGlvbl90b2tlbnMiOiAwCiAgICB9CiAgfSwKICAic2VydmljZV90aWVyIjogImRlZmF1bHQiLAogICJzeXN0ZW1fZmluZ2VycHJpbnQiOiAiZnBfNTYwYWY2ZTU1OSIKfQo= + recorded_at: Wed, 19 Nov 2025 06:19:46 GMT +recorded_with: VCR 6.3.1 diff --git a/test/integration/open_ai/chat/common_format/tools_test.rb b/test/integration/open_ai/chat/common_format/tools_test.rb new file mode 100644 index 00000000..7581731e --- /dev/null +++ b/test/integration/open_ai/chat/common_format/tools_test.rb @@ -0,0 +1,384 @@ +# frozen_string_literal: true + +require_relative "../../../test_helper" + +module Integration + module OpenAI + module Chat + module CommonFormat + class ToolsTest < ActiveSupport::TestCase + include Integration::TestHelper + + class TestAgent < ActiveAgent::Base + generate_with :openai, model: "gpt-4o-mini", api_version: :chat + + def get_weather(location:) + { location: location, temperature: "72°F", conditions: "sunny" } + end + + def calculate(operation:, a:, b:) + result = case operation + when "add" then a + b + when "subtract" then a - b + when "multiply" then a * b + when "divide" then a / b + end + { operation: operation, a: a, b: b, result: result } + end + + # Common format with 'parameters' key (recommended) + COMMON_FORMAT_PARAMETERS = { + model: "gpt-4o-mini", + messages: [ + { + role: "user", + content: "What's the weather in San Francisco?" + } + ], + tools: [ + { + type: "function", + function: { + name: "get_weather", + description: "Get the current weather in a given location", + parameters: { + type: "object", + properties: { + location: { + type: "string", + description: "The city and state, e.g. San Francisco, CA" + } + }, + required: [ "location" ] + } + } + } + ] + } + def common_format_parameters + prompt( + message: "What's the weather in San Francisco?", + tools: [ + { + name: "get_weather", + description: "Get the current weather in a given location", + parameters: { + type: "object", + properties: { + location: { + type: "string", + description: "The city and state, e.g. San Francisco, CA" + } + }, + required: [ "location" ] + } + } + ] + ) + end + + def get_weather(location:) + { location: location, temperature: "72°F", conditions: "sunny" } + end + + # Common format with 'input_schema' key (should normalize to parameters) + COMMON_FORMAT_INPUT_SCHEMA = { + model: "gpt-4o-mini", + messages: [ + { + role: "user", + content: "What's the weather in Boston?" + } + ], + tools: [ + { + type: "function", + function: { + name: "get_weather", + description: "Get the current weather in a given location", + parameters: { + type: "object", + properties: { + location: { + type: "string", + description: "The city and state, e.g. San Francisco, CA" + } + }, + required: [ "location" ] + } + } + } + ] + } + + def common_format_input_schema + prompt( + message: "What's the weather in Boston?", + tools: [ + { + name: "get_weather", + description: "Get the current weather in a given location", + input_schema: { + type: "object", + properties: { + location: { + type: "string", + description: "The city and state, e.g. San Francisco, CA" + } + }, + required: [ "location" ] + } + } + ] + ) + end + + # Multiple tools in common format + COMMON_FORMAT_MULTIPLE_TOOLS = { + model: "gpt-4o-mini", + messages: [ + { + role: "user", + content: "What's the weather in NYC and what's 5 plus 3?" + } + ], + tools: [ + { + type: "function", + function: { + name: "get_weather", + description: "Get the current weather", + parameters: { + type: "object", + properties: { + location: { type: "string" } + }, + required: [ "location" ] + } + } + }, + { + type: "function", + function: { + name: "calculate", + description: "Perform basic arithmetic", + parameters: { + type: "object", + properties: { + operation: { type: "string", enum: [ "add", "subtract", "multiply", "divide" ] }, + a: { type: "number" }, + b: { type: "number" } + }, + required: [ "operation", "a", "b" ] + } + } + } + ] + } + def common_format_multiple_tools + prompt( + message: "What's the weather in NYC and what's 5 plus 3?", + tools: [ + { + name: "get_weather", + description: "Get the current weather", + parameters: { + type: "object", + properties: { + location: { type: "string" } + }, + required: [ "location" ] + } + }, + { + name: "calculate", + description: "Perform basic arithmetic", + parameters: { + type: "object", + properties: { + operation: { type: "string", enum: [ "add", "subtract", "multiply", "divide" ] }, + a: { type: "number" }, + b: { type: "number" } + }, + required: [ "operation", "a", "b" ] + } + } + ] + ) + end + + def calculate(operation:, a:, b:) + result = case operation + when "add" then a + b + when "subtract" then a - b + when "multiply" then a * b + when "divide" then a / b + end + { operation: operation, a: a, b: b, result: result } + end + + # Tool choice - string format + COMMON_FORMAT_TOOL_CHOICE_AUTO = { + model: "gpt-4o-mini", + messages: [ + { + role: "user", + content: "What's the weather?" + } + ], + tools: [ + { + type: "function", + function: { + name: "get_weather", + description: "Get weather", + parameters: { + type: "object", + properties: { + location: { type: "string" } + }, + required: [ "location" ] + } + } + } + ], + tool_choice: "auto" + } + def common_format_tool_choice_auto + prompt( + message: "What's the weather?", + tools: [ + { + name: "get_weather", + description: "Get weather", + parameters: { + type: "object", + properties: { + location: { type: "string" } + }, + required: [ "location" ] + } + } + ], + tool_choice: "auto" + ) + end + + # Tool choice - force tool use with "required" + COMMON_FORMAT_TOOL_CHOICE_REQUIRED = { + model: "gpt-4o-mini", + messages: [ + { + role: "user", + content: "What's the weather?" + } + ], + tools: [ + { + type: "function", + function: { + name: "get_weather", + description: "Get weather", + parameters: { + type: "object", + properties: { + location: { type: "string" } + }, + required: [ "location" ] + } + } + } + ], + tool_choice: "required" + } + def common_format_tool_choice_required + prompt( + message: "What's the weather?", + tools: [ + { + name: "get_weather", + description: "Get weather", + parameters: { + type: "object", + properties: { + location: { type: "string" } + }, + required: [ "location" ] + } + } + ], + tool_choice: "required" + ) + end + + # Tool choice - specific tool + COMMON_FORMAT_TOOL_CHOICE_SPECIFIC = { + model: "gpt-4o-mini", + messages: [ + { + role: "user", + content: "What's the weather?" + } + ], + tools: [ + { + type: "function", + function: { + name: "get_weather", + description: "Get weather", + parameters: { + type: "object", + properties: { + location: { type: "string" } + }, + required: [ "location" ] + } + } + } + ], + tool_choice: { + type: "function", + function: { + name: "get_weather" + } + } + } + def common_format_tool_choice_specific + prompt( + message: "What's the weather?", + tools: [ + { + name: "get_weather", + description: "Get weather", + parameters: { + type: "object", + properties: { + location: { type: "string" } + }, + required: [ "location" ] + } + } + ], + tool_choice: { name: "get_weather" } + ) + end + end + + ################################################################################ + # This automatically runs all the tests for the test actions + ################################################################################ + [ + :common_format_parameters, + :common_format_input_schema, + :common_format_multiple_tools, + :common_format_tool_choice_auto, + :common_format_tool_choice_required, + :common_format_tool_choice_specific + ].each do |action_name| + test_request_builder(TestAgent, action_name, :generate_now, TestAgent.const_get(action_name.to_s.upcase, true)) + end + end + end + end + end +end From 57c7e9d1eef98c97b83919840ca86c12ade2a818 Mon Sep 17 00:00:00 2001 From: Zane Wolfgang Pickett Date: Wed, 19 Nov 2025 09:17:36 -0800 Subject: [PATCH 04/17] Add OpenRouter Tools Common Format --- .../providers/open_router/request.rb | 20 + .../providers/open_router/transforms.rb | 30 ++ .../providers/open_router_provider.rb | 34 ++ .../test_agent_common_format_input_schema.yml | 159 ++++++++ ...est_agent_common_format_multiple_tools.yml | 157 ++++++++ .../test_agent_common_format_parameters.yml | 161 ++++++++ ...t_agent_common_format_tool_choice_auto.yml | 79 ++++ ...ent_common_format_tool_choice_required.yml | 79 ++++ ...ent_common_format_tool_choice_specific.yml | 155 ++++++++ .../open_router/common_format/tools_test.rb | 367 ++++++++++++++++++ 10 files changed, 1241 insertions(+) create mode 100644 test/fixtures/vcr_cassettes/integration/open_router/common_format/tools_test/test_agent_common_format_input_schema.yml create mode 100644 test/fixtures/vcr_cassettes/integration/open_router/common_format/tools_test/test_agent_common_format_multiple_tools.yml create mode 100644 test/fixtures/vcr_cassettes/integration/open_router/common_format/tools_test/test_agent_common_format_parameters.yml create mode 100644 test/fixtures/vcr_cassettes/integration/open_router/common_format/tools_test/test_agent_common_format_tool_choice_auto.yml create mode 100644 test/fixtures/vcr_cassettes/integration/open_router/common_format/tools_test/test_agent_common_format_tool_choice_required.yml create mode 100644 test/fixtures/vcr_cassettes/integration/open_router/common_format/tools_test/test_agent_common_format_tool_choice_specific.yml create mode 100644 test/integration/open_router/common_format/tools_test.rb diff --git a/lib/active_agent/providers/open_router/request.rb b/lib/active_agent/providers/open_router/request.rb index 4896abf2..7ba8fb11 100644 --- a/lib/active_agent/providers/open_router/request.rb +++ b/lib/active_agent/providers/open_router/request.rb @@ -148,6 +148,26 @@ def instructions=(*values) self.messages = instructions_messages + current_messages end + # Gets tool_choice bypassing gem validation + # + # OpenRouter supports "any" which isn't valid in OpenAI gem types. + # + # @return [String, Hash, nil] + def tool_choice + __getobj__.instance_variable_get(:@data)[:tool_choice] + end + + # Sets tool_choice bypassing gem validation + # + # OpenRouter supports "any" which isn't valid in OpenAI gem types, + # so we bypass the gem's type validation by setting @data directly. + # + # @param value [String, Hash, nil] + # @return [void] + def tool_choice=(value) + __getobj__.instance_variable_get(:@data)[:tool_choice] = value + end + # Accessor for OpenRouter-specific provider preferences # # @return [Hash, nil] diff --git a/lib/active_agent/providers/open_router/transforms.rb b/lib/active_agent/providers/open_router/transforms.rb index c820364f..f5e4f388 100644 --- a/lib/active_agent/providers/open_router/transforms.rb +++ b/lib/active_agent/providers/open_router/transforms.rb @@ -62,9 +62,39 @@ def normalize_params(params) # Use OpenAI transforms for the base parameters openai_params = OpenAI::Chat::Transforms.normalize_params(params) + # Override tool_choice normalization for OpenRouter's "any" vs "required" difference + if openai_params[:tool_choice] + openai_params[:tool_choice] = normalize_tool_choice(openai_params[:tool_choice]) + end + [ openai_params, openrouter_params ] end + # Normalizes tools using OpenAI transforms + # + # @param tools [Array] + # @return [Array] + def normalize_tools(tools) + OpenAI::Chat::Transforms.normalize_tools(tools) + end + + # Normalizes tool_choice for OpenRouter API differences + # + # OpenRouter uses "any" instead of OpenAI's "required" for forcing tool use. + # Converts common format to OpenRouter-specific format: + # - "required" (common) → "any" (OpenRouter) + # - Everything else delegates to OpenAI transforms + # + # @param tool_choice [String, Hash, Symbol] + # @return [String, Hash, Symbol] + def normalize_tool_choice(tool_choice) + # Convert "required" to OpenRouter's "any" + return "any" if tool_choice.to_s == "required" + + # For everything else, use OpenAI transforms + OpenAI::Chat::Transforms.normalize_tool_choice(tool_choice) + end + # Normalizes messages using OpenAI transforms # # @param messages [Array, String, Hash, nil] diff --git a/lib/active_agent/providers/open_router_provider.rb b/lib/active_agent/providers/open_router_provider.rb index cce5ff92..1204d6bc 100644 --- a/lib/active_agent/providers/open_router_provider.rb +++ b/lib/active_agent/providers/open_router_provider.rb @@ -33,6 +33,40 @@ def self.prompt_request_type protected + # @see BaseProvider#prepare_prompt_request + # @return [Request] + def prepare_prompt_request + prepare_prompt_request_tools + super + end + + # @api private + def prepare_prompt_request_tools + return unless request.tool_choice + + # Get list of function calls that have been made + # In Chat API, tool calls are in the assistant message's tool_calls array + functions_used = message_stack + .select { |msg| msg[:role] == "assistant" && msg[:tool_calls] } + .flat_map { |msg| msg[:tool_calls] } + .map { |tc| tc.dig(:function, :name) } + .compact + + # Check if tool_choice is a hash (specific tool) or string (auto/any) + if request.tool_choice.is_a?(Hash) + # Specific tool choice - clear if that tool was used + tool_choice_name = request.tool_choice.dig(:function, :name) + if tool_choice_name && functions_used.include?(tool_choice_name) + request.tool_choice = nil + end + elsif request.tool_choice == "any" + # OpenRouter uses "any" for required - clear if any tool was used + if functions_used.any? + request.tool_choice = nil + end + end + end + # Merges streaming delta into the message with role cleanup. # # Overrides parent to handle OpenRouter's role copying behavior which duplicates diff --git a/test/fixtures/vcr_cassettes/integration/open_router/common_format/tools_test/test_agent_common_format_input_schema.yml b/test/fixtures/vcr_cassettes/integration/open_router/common_format/tools_test/test_agent_common_format_input_schema.yml new file mode 100644 index 00000000..56882366 --- /dev/null +++ b/test/fixtures/vcr_cassettes/integration/open_router/common_format/tools_test/test_agent_common_format_input_schema.yml @@ -0,0 +1,159 @@ +--- +http_interactions: +- request: + method: post + uri: https://openrouter.ai/api/v1/chat/completions + body: + encoding: UTF-8 + string: '{"model":"google/gemini-2.0-flash-001","messages":[{"role":"user","content":"What''s + the weather in Boston?"}],"tools":[{"type":"function","function":{"name":"get_weather","description":"Get + the current weather in a given location","parameters":{"type":"object","properties":{"location":{"type":"string","description":"The + city and state, e.g. San Francisco, CA"}},"required":["location"]}}}]}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json + User-Agent: + - OpenAI::Client/Ruby 0.35.1 + Host: + - openrouter.ai + X-Stainless-Arch: + - arm64 + X-Stainless-Lang: + - ruby + X-Stainless-Os: + - MacOS + X-Stainless-Package-Version: + - 0.35.1 + X-Stainless-Runtime: + - ruby + X-Stainless-Runtime-Version: + - 3.4.7 + Content-Type: + - application/json + Authorization: + - Bearer ACCESS_TOKEN + Http-Referer: + - https://example.com + X-Title: + - Dummy + X-Stainless-Retry-Count: + - '0' + X-Stainless-Timeout: + - '600.0' + Content-Length: + - '392' + response: + status: + code: 200 + message: OK + headers: + Date: + - Wed, 19 Nov 2025 17:04:08 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Access-Control-Allow-Origin: + - "*" + Vary: + - Accept-Encoding + Permissions-Policy: + - payment=(self "https://checkout.stripe.com" "https://connect-js.stripe.com" + "https://js.stripe.com" "https://*.js.stripe.com" "https://hooks.stripe.com") + Referrer-Policy: + - no-referrer, strict-origin-when-cross-origin + X-Content-Type-Options: + - nosniff + Server: + - cloudflare + Cf-Ray: + - 9a1148f44c17ca3e-SJC + body: + encoding: ASCII-8BIT + string: "\n \n{\"id\":\"gen-1763571848-XSghN22Ea3eUi5fgPXm0\",\"provider\":\"Google\",\"model\":\"google/gemini-2.0-flash-001\",\"object\":\"chat.completion\",\"created\":1763571848,\"choices\":[{\"logprobs\":null,\"finish_reason\":\"tool_calls\",\"native_finish_reason\":\"STOP\",\"index\":0,\"message\":{\"role\":\"assistant\",\"content\":\"\",\"refusal\":null,\"reasoning\":null,\"tool_calls\":[{\"index\":0,\"id\":\"tool_get_weather_yl7m3XDXuvnK3iiaGsjB\",\"type\":\"function\",\"function\":{\"name\":\"get_weather\",\"arguments\":\"{\\\"location\\\":\\\"Boston, + MA\\\"}\"}}]}}],\"usage\":{\"prompt_tokens\":36,\"completion_tokens\":7,\"total_tokens\":43,\"prompt_tokens_details\":{\"cached_tokens\":0},\"completion_tokens_details\":{\"reasoning_tokens\":0,\"image_tokens\":0}}}" + recorded_at: Wed, 19 Nov 2025 17:04:08 GMT +- request: + method: post + uri: https://openrouter.ai/api/v1/chat/completions + body: + encoding: UTF-8 + string: '{"model":"google/gemini-2.0-flash-001","messages":[{"role":"user","content":"What''s + the weather in Boston?"},{"role":"assistant","content":"","refusal":null,"tool_calls":[{"id":"tool_get_weather_yl7m3XDXuvnK3iiaGsjB","function":{"arguments":"{\"location\":\"Boston, + MA\"}","name":"get_weather"},"type":"function","index":0}],"reasoning":null},{"role":"tool","content":"{\"location\":\"Boston, + MA\",\"temperature\":\"72°F\",\"conditions\":\"sunny\"}","tool_call_id":"tool_get_weather_yl7m3XDXuvnK3iiaGsjB"}],"tools":[{"type":"function","function":{"name":"get_weather","description":"Get + the current weather in a given location","parameters":{"type":"object","properties":{"location":{"type":"string","description":"The + city and state, e.g. San Francisco, CA"}},"required":["location"]}}}]}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json + User-Agent: + - OpenAI::Client/Ruby 0.35.1 + Host: + - openrouter.ai + X-Stainless-Arch: + - arm64 + X-Stainless-Lang: + - ruby + X-Stainless-Os: + - MacOS + X-Stainless-Package-Version: + - 0.35.1 + X-Stainless-Runtime: + - ruby + X-Stainless-Runtime-Version: + - 3.4.7 + Content-Type: + - application/json + Authorization: + - Bearer ACCESS_TOKEN + Http-Referer: + - https://example.com + X-Title: + - Dummy + X-Stainless-Retry-Count: + - '0' + X-Stainless-Timeout: + - '600.0' + Content-Length: + - '790' + response: + status: + code: 200 + message: OK + headers: + Date: + - Wed, 19 Nov 2025 17:04:09 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Access-Control-Allow-Origin: + - "*" + Vary: + - Accept-Encoding + Permissions-Policy: + - payment=(self "https://checkout.stripe.com" "https://connect-js.stripe.com" + "https://js.stripe.com" "https://*.js.stripe.com" "https://hooks.stripe.com") + Referrer-Policy: + - no-referrer, strict-origin-when-cross-origin + X-Content-Type-Options: + - nosniff + Server: + - cloudflare + Cf-Ray: + - 9a1148f809191949-SJC + body: + encoding: ASCII-8BIT + string: !binary |- + CiAgICAgICAgIAp7ImlkIjoiZ2VuLTE3NjM1NzE4NDgtajFuaWt4dDBNeUVSMDh0Vmo2dW0iLCJwcm92aWRlciI6Ikdvb2dsZSIsIm1vZGVsIjoiZ29vZ2xlL2dlbWluaS0yLjAtZmxhc2gtMDAxIiwib2JqZWN0IjoiY2hhdC5jb21wbGV0aW9uIiwiY3JlYXRlZCI6MTc2MzU3MTg0OSwiY2hvaWNlcyI6W3sibG9ncHJvYnMiOm51bGwsImZpbmlzaF9yZWFzb24iOiJzdG9wIiwibmF0aXZlX2ZpbmlzaF9yZWFzb24iOiJTVE9QIiwiaW5kZXgiOjAsIm1lc3NhZ2UiOnsicm9sZSI6ImFzc2lzdGFudCIsImNvbnRlbnQiOiJUaGUgd2VhdGhlciBpbiBCb3N0b24sIE1BIGlzIHN1bm55IHdpdGggYSB0ZW1wZXJhdHVyZSBvZiA3MsKwRi5cbiIsInJlZnVzYWwiOm51bGwsInJlYXNvbmluZyI6bnVsbH19XSwidXNhZ2UiOnsicHJvbXB0X3Rva2VucyI6NzUsImNvbXBsZXRpb25fdG9rZW5zIjoxOSwidG90YWxfdG9rZW5zIjo5NCwicHJvbXB0X3Rva2Vuc19kZXRhaWxzIjp7ImNhY2hlZF90b2tlbnMiOjB9LCJjb21wbGV0aW9uX3Rva2Vuc19kZXRhaWxzIjp7InJlYXNvbmluZ190b2tlbnMiOjAsImltYWdlX3Rva2VucyI6MH19fQ== + recorded_at: Wed, 19 Nov 2025 17:04:09 GMT +recorded_with: VCR 6.3.1 diff --git a/test/fixtures/vcr_cassettes/integration/open_router/common_format/tools_test/test_agent_common_format_multiple_tools.yml b/test/fixtures/vcr_cassettes/integration/open_router/common_format/tools_test/test_agent_common_format_multiple_tools.yml new file mode 100644 index 00000000..f8a35331 --- /dev/null +++ b/test/fixtures/vcr_cassettes/integration/open_router/common_format/tools_test/test_agent_common_format_multiple_tools.yml @@ -0,0 +1,157 @@ +--- +http_interactions: +- request: + method: post + uri: https://openrouter.ai/api/v1/chat/completions + body: + encoding: UTF-8 + string: '{"model":"google/gemini-2.0-flash-001","messages":[{"role":"user","content":"What''s + the weather in NYC and what''s 5 plus 3?"}],"tools":[{"type":"function","function":{"name":"get_weather","description":"Get + the current weather","parameters":{"type":"object","properties":{"location":{"type":"string"}},"required":["location"]}}},{"type":"function","function":{"name":"calculate","description":"Perform + basic arithmetic","parameters":{"type":"object","properties":{"operation":{"type":"string","enum":["add","subtract","multiply","divide"]},"a":{"type":"number"},"b":{"type":"number"}},"required":["operation","a","b"]}}}]}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json + User-Agent: + - OpenAI::Client/Ruby 0.35.1 + Host: + - openrouter.ai + X-Stainless-Arch: + - arm64 + X-Stainless-Lang: + - ruby + X-Stainless-Os: + - MacOS + X-Stainless-Package-Version: + - 0.35.1 + X-Stainless-Runtime: + - ruby + X-Stainless-Runtime-Version: + - 3.4.7 + Content-Type: + - application/json + Authorization: + - Bearer ACCESS_TOKEN + Http-Referer: + - https://example.com + X-Title: + - Dummy + X-Stainless-Retry-Count: + - '0' + X-Stainless-Timeout: + - '600.0' + Content-Length: + - '622' + response: + status: + code: 200 + message: OK + headers: + Date: + - Wed, 19 Nov 2025 17:04:10 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Access-Control-Allow-Origin: + - "*" + Vary: + - Accept-Encoding + Permissions-Policy: + - payment=(self "https://checkout.stripe.com" "https://connect-js.stripe.com" + "https://js.stripe.com" "https://*.js.stripe.com" "https://hooks.stripe.com") + Referrer-Policy: + - no-referrer, strict-origin-when-cross-origin + X-Content-Type-Options: + - nosniff + Server: + - cloudflare + Cf-Ray: + - 9a1148fc0bea2847-SJC + body: + encoding: ASCII-8BIT + string: "\n \n{\"id\":\"gen-1763571849-fMuPYD0BKShyHqTXgXvN\",\"provider\":\"Google + AI Studio\",\"model\":\"google/gemini-2.0-flash-001\",\"object\":\"chat.completion\",\"created\":1763571849,\"choices\":[{\"logprobs\":null,\"finish_reason\":\"tool_calls\",\"native_finish_reason\":\"STOP\",\"index\":0,\"message\":{\"role\":\"assistant\",\"content\":\"\",\"refusal\":null,\"reasoning\":null,\"tool_calls\":[{\"index\":0,\"id\":\"tool_get_weather_tIDAj9UZ8o1c2mXwibMI\",\"type\":\"function\",\"function\":{\"name\":\"get_weather\",\"arguments\":\"{\\\"location\\\":\\\"NYC\\\"}\"}},{\"index\":1,\"id\":\"tool_calculate_ltXKJIZfWAss1yWNoDSt\",\"type\":\"function\",\"function\":{\"name\":\"calculate\",\"arguments\":\"{\\\"a\\\":5,\\\"operation\\\":\\\"add\\\",\\\"b\\\":3}\"}}]}}],\"usage\":{\"prompt_tokens\":46,\"completion_tokens\":12,\"total_tokens\":58,\"prompt_tokens_details\":{\"cached_tokens\":0},\"completion_tokens_details\":{\"reasoning_tokens\":0,\"image_tokens\":0}}}" + recorded_at: Wed, 19 Nov 2025 17:04:10 GMT +- request: + method: post + uri: https://openrouter.ai/api/v1/chat/completions + body: + encoding: UTF-8 + string: '{"model":"google/gemini-2.0-flash-001","messages":[{"role":"user","content":"What''s + the weather in NYC and what''s 5 plus 3?"},{"role":"assistant","content":"","refusal":null,"tool_calls":[{"id":"tool_get_weather_tIDAj9UZ8o1c2mXwibMI","function":{"arguments":"{\"location\":\"NYC\"}","name":"get_weather"},"type":"function","index":0},{"id":"tool_calculate_ltXKJIZfWAss1yWNoDSt","function":{"arguments":"{\"a\":5,\"operation\":\"add\",\"b\":3}","name":"calculate"},"type":"function","index":1}],"reasoning":null},{"role":"tool","content":"{\"location\":\"NYC\",\"temperature\":\"72°F\",\"conditions\":\"sunny\"}","tool_call_id":"tool_get_weather_tIDAj9UZ8o1c2mXwibMI"},{"role":"tool","content":"{\"operation\":\"add\",\"a\":5,\"b\":3,\"result\":8}","tool_call_id":"tool_calculate_ltXKJIZfWAss1yWNoDSt"}],"tools":[{"type":"function","function":{"name":"get_weather","description":"Get + the current weather","parameters":{"type":"object","properties":{"location":{"type":"string"}},"required":["location"]}}},{"type":"function","function":{"name":"calculate","description":"Perform + basic arithmetic","parameters":{"type":"object","properties":{"operation":{"type":"string","enum":["add","subtract","multiply","divide"]},"a":{"type":"number"},"b":{"type":"number"}},"required":["operation","a","b"]}}}]}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json + User-Agent: + - OpenAI::Client/Ruby 0.35.1 + Host: + - openrouter.ai + X-Stainless-Arch: + - arm64 + X-Stainless-Lang: + - ruby + X-Stainless-Os: + - MacOS + X-Stainless-Package-Version: + - 0.35.1 + X-Stainless-Runtime: + - ruby + X-Stainless-Runtime-Version: + - 3.4.7 + Content-Type: + - application/json + Authorization: + - Bearer ACCESS_TOKEN + Http-Referer: + - https://example.com + X-Title: + - Dummy + X-Stainless-Retry-Count: + - '0' + X-Stainless-Timeout: + - '600.0' + Content-Length: + - '1299' + response: + status: + code: 200 + message: OK + headers: + Date: + - Wed, 19 Nov 2025 17:04:10 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Access-Control-Allow-Origin: + - "*" + Vary: + - Accept-Encoding + Permissions-Policy: + - payment=(self "https://checkout.stripe.com" "https://connect-js.stripe.com" + "https://js.stripe.com" "https://*.js.stripe.com" "https://hooks.stripe.com") + Referrer-Policy: + - no-referrer, strict-origin-when-cross-origin + X-Content-Type-Options: + - nosniff + Server: + - cloudflare + Cf-Ray: + - 9a1149011f7dcf26-SJC + body: + encoding: ASCII-8BIT + string: !binary |- + CiAgICAgICAgIAp7ImlkIjoiZ2VuLTE3NjM1NzE4NTAtWGUwenNoMmVyQ3htTDE0czJZeFUiLCJwcm92aWRlciI6Ikdvb2dsZSBBSSBTdHVkaW8iLCJtb2RlbCI6Imdvb2dsZS9nZW1pbmktMi4wLWZsYXNoLTAwMSIsIm9iamVjdCI6ImNoYXQuY29tcGxldGlvbiIsImNyZWF0ZWQiOjE3NjM1NzE4NTAsImNob2ljZXMiOlt7ImxvZ3Byb2JzIjpudWxsLCJmaW5pc2hfcmVhc29uIjoic3RvcCIsIm5hdGl2ZV9maW5pc2hfcmVhc29uIjoiU1RPUCIsImluZGV4IjowLCJtZXNzYWdlIjp7InJvbGUiOiJhc3Npc3RhbnQiLCJjb250ZW50IjoiVGhlIHdlYXRoZXIgaW4gTllDIGlzIHN1bm55IHdpdGggYSB0ZW1wZXJhdHVyZSBvZiA3MsKwRi4gNSBwbHVzIDMgaXMgOC5cbiIsInJlZnVzYWwiOm51bGwsInJlYXNvbmluZyI6bnVsbH19XSwidXNhZ2UiOnsicHJvbXB0X3Rva2VucyI6MTEyLCJjb21wbGV0aW9uX3Rva2VucyI6MjYsInRvdGFsX3Rva2VucyI6MTM4LCJwcm9tcHRfdG9rZW5zX2RldGFpbHMiOnsiY2FjaGVkX3Rva2VucyI6MH0sImNvbXBsZXRpb25fdG9rZW5zX2RldGFpbHMiOnsicmVhc29uaW5nX3Rva2VucyI6MCwiaW1hZ2VfdG9rZW5zIjowfX19 + recorded_at: Wed, 19 Nov 2025 17:04:11 GMT +recorded_with: VCR 6.3.1 diff --git a/test/fixtures/vcr_cassettes/integration/open_router/common_format/tools_test/test_agent_common_format_parameters.yml b/test/fixtures/vcr_cassettes/integration/open_router/common_format/tools_test/test_agent_common_format_parameters.yml new file mode 100644 index 00000000..5adb7d17 --- /dev/null +++ b/test/fixtures/vcr_cassettes/integration/open_router/common_format/tools_test/test_agent_common_format_parameters.yml @@ -0,0 +1,161 @@ +--- +http_interactions: +- request: + method: post + uri: https://openrouter.ai/api/v1/chat/completions + body: + encoding: UTF-8 + string: '{"model":"google/gemini-2.0-flash-001","messages":[{"role":"user","content":"What''s + the weather in San Francisco?"}],"tools":[{"type":"function","function":{"name":"get_weather","description":"Get + the current weather in a given location","parameters":{"type":"object","properties":{"location":{"type":"string","description":"The + city and state, e.g. San Francisco, CA"}},"required":["location"]}}}]}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json + User-Agent: + - OpenAI::Client/Ruby 0.35.1 + Host: + - openrouter.ai + X-Stainless-Arch: + - arm64 + X-Stainless-Lang: + - ruby + X-Stainless-Os: + - MacOS + X-Stainless-Package-Version: + - 0.35.1 + X-Stainless-Runtime: + - ruby + X-Stainless-Runtime-Version: + - 3.4.7 + Content-Type: + - application/json + Authorization: + - Bearer ACCESS_TOKEN + Http-Referer: + - https://example.com + X-Title: + - Dummy + X-Stainless-Retry-Count: + - '0' + X-Stainless-Timeout: + - '600.0' + Content-Length: + - '399' + response: + status: + code: 200 + message: OK + headers: + Date: + - Wed, 19 Nov 2025 17:04:17 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Access-Control-Allow-Origin: + - "*" + Vary: + - Accept-Encoding + Permissions-Policy: + - payment=(self "https://checkout.stripe.com" "https://connect-js.stripe.com" + "https://js.stripe.com" "https://*.js.stripe.com" "https://hooks.stripe.com") + Referrer-Policy: + - no-referrer, strict-origin-when-cross-origin + X-Content-Type-Options: + - nosniff + Server: + - cloudflare + Cf-Ray: + - 9a11490bbbe5efbd-SJC + body: + encoding: ASCII-8BIT + string: "\n \n\n \n\n \n\n \n\n \n\n + \ \n\n \n\n \n\n \n\n \n\n \n\n + \ \n\n \n{\"id\":\"gen-1763571852-oRsToHyDzpZb1RvUO6ow\",\"provider\":\"Google\",\"model\":\"google/gemini-2.0-flash-001\",\"object\":\"chat.completion\",\"created\":1763571857,\"choices\":[{\"logprobs\":null,\"finish_reason\":\"tool_calls\",\"native_finish_reason\":\"STOP\",\"index\":0,\"message\":{\"role\":\"assistant\",\"content\":\"\",\"refusal\":null,\"reasoning\":null,\"tool_calls\":[{\"index\":0,\"id\":\"tool_get_weather_S9ahEhRffDshS18YeSbL\",\"type\":\"function\",\"function\":{\"name\":\"get_weather\",\"arguments\":\"{\\\"location\\\":\\\"San + Francisco, CA\\\"}\"}}]}}],\"usage\":{\"prompt_tokens\":37,\"completion_tokens\":8,\"total_tokens\":45,\"prompt_tokens_details\":{\"cached_tokens\":0},\"completion_tokens_details\":{\"reasoning_tokens\":0,\"image_tokens\":0}}}" + recorded_at: Wed, 19 Nov 2025 17:04:17 GMT +- request: + method: post + uri: https://openrouter.ai/api/v1/chat/completions + body: + encoding: UTF-8 + string: '{"model":"google/gemini-2.0-flash-001","messages":[{"role":"user","content":"What''s + the weather in San Francisco?"},{"role":"assistant","content":"","refusal":null,"tool_calls":[{"id":"tool_get_weather_S9ahEhRffDshS18YeSbL","function":{"arguments":"{\"location\":\"San + Francisco, CA\"}","name":"get_weather"},"type":"function","index":0}],"reasoning":null},{"role":"tool","content":"{\"location\":\"San + Francisco, CA\",\"temperature\":\"72°F\",\"conditions\":\"sunny\"}","tool_call_id":"tool_get_weather_S9ahEhRffDshS18YeSbL"}],"tools":[{"type":"function","function":{"name":"get_weather","description":"Get + the current weather in a given location","parameters":{"type":"object","properties":{"location":{"type":"string","description":"The + city and state, e.g. San Francisco, CA"}},"required":["location"]}}}]}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json + User-Agent: + - OpenAI::Client/Ruby 0.35.1 + Host: + - openrouter.ai + X-Stainless-Arch: + - arm64 + X-Stainless-Lang: + - ruby + X-Stainless-Os: + - MacOS + X-Stainless-Package-Version: + - 0.35.1 + X-Stainless-Runtime: + - ruby + X-Stainless-Runtime-Version: + - 3.4.7 + Content-Type: + - application/json + Authorization: + - Bearer ACCESS_TOKEN + Http-Referer: + - https://example.com + X-Title: + - Dummy + X-Stainless-Retry-Count: + - '0' + X-Stainless-Timeout: + - '600.0' + Content-Length: + - '811' + response: + status: + code: 200 + message: OK + headers: + Date: + - Wed, 19 Nov 2025 17:04:18 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Access-Control-Allow-Origin: + - "*" + Vary: + - Accept-Encoding + Permissions-Policy: + - payment=(self "https://checkout.stripe.com" "https://connect-js.stripe.com" + "https://js.stripe.com" "https://*.js.stripe.com" "https://hooks.stripe.com") + Referrer-Policy: + - no-referrer, strict-origin-when-cross-origin + X-Content-Type-Options: + - nosniff + Server: + - cloudflare + Cf-Ray: + - 9a1149303ac3cdb3-SJC + body: + encoding: ASCII-8BIT + string: !binary |- + CiAgICAgICAgIAp7ImlkIjoiZ2VuLTE3NjM1NzE4NTctUmpwSGsxVkZncW42eTJxQldnUk0iLCJwcm92aWRlciI6Ikdvb2dsZSBBSSBTdHVkaW8iLCJtb2RlbCI6Imdvb2dsZS9nZW1pbmktMi4wLWZsYXNoLTAwMSIsIm9iamVjdCI6ImNoYXQuY29tcGxldGlvbiIsImNyZWF0ZWQiOjE3NjM1NzE4NTgsImNob2ljZXMiOlt7ImxvZ3Byb2JzIjpudWxsLCJmaW5pc2hfcmVhc29uIjoic3RvcCIsIm5hdGl2ZV9maW5pc2hfcmVhc29uIjoiU1RPUCIsImluZGV4IjowLCJtZXNzYWdlIjp7InJvbGUiOiJhc3Npc3RhbnQiLCJjb250ZW50IjoiVGhlIHdlYXRoZXIgaW4gU2FuIEZyYW5jaXNjbywgQ0EgaXMgc3Vubnkgd2l0aCBhIHRlbXBlcmF0dXJlIG9mIDcywrBGLlxuIiwicmVmdXNhbCI6bnVsbCwicmVhc29uaW5nIjpudWxsfX1dLCJ1c2FnZSI6eyJwcm9tcHRfdG9rZW5zIjo3NywiY29tcGxldGlvbl90b2tlbnMiOjIwLCJ0b3RhbF90b2tlbnMiOjk3LCJwcm9tcHRfdG9rZW5zX2RldGFpbHMiOnsiY2FjaGVkX3Rva2VucyI6MH0sImNvbXBsZXRpb25fdG9rZW5zX2RldGFpbHMiOnsicmVhc29uaW5nX3Rva2VucyI6MCwiaW1hZ2VfdG9rZW5zIjowfX19 + recorded_at: Wed, 19 Nov 2025 17:04:18 GMT +recorded_with: VCR 6.3.1 diff --git a/test/fixtures/vcr_cassettes/integration/open_router/common_format/tools_test/test_agent_common_format_tool_choice_auto.yml b/test/fixtures/vcr_cassettes/integration/open_router/common_format/tools_test/test_agent_common_format_tool_choice_auto.yml new file mode 100644 index 00000000..fc9a872a --- /dev/null +++ b/test/fixtures/vcr_cassettes/integration/open_router/common_format/tools_test/test_agent_common_format_tool_choice_auto.yml @@ -0,0 +1,79 @@ +--- +http_interactions: +- request: + method: post + uri: https://openrouter.ai/api/v1/chat/completions + body: + encoding: UTF-8 + string: '{"model":"google/gemini-2.0-flash-001","messages":[{"role":"user","content":"What''s + the weather?"}],"tools":[{"type":"function","function":{"name":"get_weather","description":"Get + weather","parameters":{"type":"object","properties":{"location":{"type":"string"}},"required":["location"]}}}],"tool_choice":"auto"}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json + User-Agent: + - OpenAI::Client/Ruby 0.35.1 + Host: + - openrouter.ai + X-Stainless-Arch: + - arm64 + X-Stainless-Lang: + - ruby + X-Stainless-Os: + - MacOS + X-Stainless-Package-Version: + - 0.35.1 + X-Stainless-Runtime: + - ruby + X-Stainless-Runtime-Version: + - 3.4.7 + Content-Type: + - application/json + Authorization: + - Bearer ACCESS_TOKEN + Http-Referer: + - https://example.com + X-Title: + - Dummy + X-Stainless-Retry-Count: + - '0' + X-Stainless-Timeout: + - '600.0' + Content-Length: + - '312' + response: + status: + code: 200 + message: OK + headers: + Date: + - Wed, 19 Nov 2025 17:04:11 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Access-Control-Allow-Origin: + - "*" + Vary: + - Accept-Encoding + Permissions-Policy: + - payment=(self "https://checkout.stripe.com" "https://connect-js.stripe.com" + "https://js.stripe.com" "https://*.js.stripe.com" "https://hooks.stripe.com") + Referrer-Policy: + - no-referrer, strict-origin-when-cross-origin + X-Content-Type-Options: + - nosniff + Server: + - cloudflare + Cf-Ray: + - 9a1149061e6f9cd0-SJC + body: + encoding: ASCII-8BIT + string: "\n \n{\"id\":\"gen-1763571851-y4ujPTAg7Fty3VNZ7xfH\",\"provider\":\"Google\",\"model\":\"google/gemini-2.0-flash-001\",\"object\":\"chat.completion\",\"created\":1763571851,\"choices\":[{\"logprobs\":null,\"finish_reason\":\"stop\",\"native_finish_reason\":\"STOP\",\"index\":0,\"message\":{\"role\":\"assistant\",\"content\":\"What + is the location you want to know the weather for?\\n\",\"refusal\":null,\"reasoning\":null}}],\"usage\":{\"prompt_tokens\":15,\"completion_tokens\":13,\"total_tokens\":28,\"prompt_tokens_details\":{\"cached_tokens\":0},\"completion_tokens_details\":{\"reasoning_tokens\":0,\"image_tokens\":0}}}" + recorded_at: Wed, 19 Nov 2025 17:04:11 GMT +recorded_with: VCR 6.3.1 diff --git a/test/fixtures/vcr_cassettes/integration/open_router/common_format/tools_test/test_agent_common_format_tool_choice_required.yml b/test/fixtures/vcr_cassettes/integration/open_router/common_format/tools_test/test_agent_common_format_tool_choice_required.yml new file mode 100644 index 00000000..a2380893 --- /dev/null +++ b/test/fixtures/vcr_cassettes/integration/open_router/common_format/tools_test/test_agent_common_format_tool_choice_required.yml @@ -0,0 +1,79 @@ +--- +http_interactions: +- request: + method: post + uri: https://openrouter.ai/api/v1/chat/completions + body: + encoding: UTF-8 + string: '{"model":"google/gemini-2.0-flash-001","messages":[{"role":"user","content":"What''s + the weather?"}],"tools":[{"type":"function","function":{"name":"get_weather","description":"Get + weather","parameters":{"type":"object","properties":{"location":{"type":"string"}},"required":["location"]}}}],"tool_choice":"any"}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json + User-Agent: + - OpenAI::Client/Ruby 0.35.1 + Host: + - openrouter.ai + X-Stainless-Arch: + - arm64 + X-Stainless-Lang: + - ruby + X-Stainless-Os: + - MacOS + X-Stainless-Package-Version: + - 0.35.1 + X-Stainless-Runtime: + - ruby + X-Stainless-Runtime-Version: + - 3.4.7 + Content-Type: + - application/json + Authorization: + - Bearer ACCESS_TOKEN + Http-Referer: + - https://example.com + X-Title: + - Dummy + X-Stainless-Retry-Count: + - '0' + X-Stainless-Timeout: + - '600.0' + Content-Length: + - '311' + response: + status: + code: 200 + message: OK + headers: + Date: + - Wed, 19 Nov 2025 17:04:06 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Access-Control-Allow-Origin: + - "*" + Vary: + - Accept-Encoding + Permissions-Policy: + - payment=(self "https://checkout.stripe.com" "https://connect-js.stripe.com" + "https://js.stripe.com" "https://*.js.stripe.com" "https://hooks.stripe.com") + Referrer-Policy: + - no-referrer, strict-origin-when-cross-origin + X-Content-Type-Options: + - nosniff + Server: + - cloudflare + Cf-Ray: + - 9a1148e33af34f08-SJC + body: + encoding: ASCII-8BIT + string: "\n \n{\"id\":\"gen-1763571845-RJNe41mzAIbHwWZykimo\",\"provider\":\"Google\",\"model\":\"google/gemini-2.0-flash-001\",\"object\":\"chat.completion\",\"created\":1763571845,\"choices\":[{\"logprobs\":null,\"finish_reason\":\"stop\",\"native_finish_reason\":\"STOP\",\"index\":0,\"message\":{\"role\":\"assistant\",\"content\":\"Could + you please tell me where are you located?\\n\",\"refusal\":null,\"reasoning\":null}}],\"usage\":{\"prompt_tokens\":15,\"completion_tokens\":11,\"total_tokens\":26,\"prompt_tokens_details\":{\"cached_tokens\":0},\"completion_tokens_details\":{\"reasoning_tokens\":0,\"image_tokens\":0}}}" + recorded_at: Wed, 19 Nov 2025 17:04:06 GMT +recorded_with: VCR 6.3.1 diff --git a/test/fixtures/vcr_cassettes/integration/open_router/common_format/tools_test/test_agent_common_format_tool_choice_specific.yml b/test/fixtures/vcr_cassettes/integration/open_router/common_format/tools_test/test_agent_common_format_tool_choice_specific.yml new file mode 100644 index 00000000..a39f9c06 --- /dev/null +++ b/test/fixtures/vcr_cassettes/integration/open_router/common_format/tools_test/test_agent_common_format_tool_choice_specific.yml @@ -0,0 +1,155 @@ +--- +http_interactions: +- request: + method: post + uri: https://openrouter.ai/api/v1/chat/completions + body: + encoding: UTF-8 + string: '{"model":"google/gemini-2.0-flash-001","messages":[{"role":"user","content":"What''s + the weather?"}],"tools":[{"type":"function","function":{"name":"get_weather","description":"Get + weather","parameters":{"type":"object","properties":{"location":{"type":"string"}},"required":["location"]}}}],"tool_choice":{"type":"function","function":{"name":"get_weather"}}}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json + User-Agent: + - OpenAI::Client/Ruby 0.35.1 + Host: + - openrouter.ai + X-Stainless-Arch: + - arm64 + X-Stainless-Lang: + - ruby + X-Stainless-Os: + - MacOS + X-Stainless-Package-Version: + - 0.35.1 + X-Stainless-Runtime: + - ruby + X-Stainless-Runtime-Version: + - 3.4.7 + Content-Type: + - application/json + Authorization: + - Bearer ACCESS_TOKEN + Http-Referer: + - https://example.com + X-Title: + - Dummy + X-Stainless-Retry-Count: + - '0' + X-Stainless-Timeout: + - '600.0' + Content-Length: + - '359' + response: + status: + code: 200 + message: OK + headers: + Date: + - Wed, 19 Nov 2025 17:04:07 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Access-Control-Allow-Origin: + - "*" + Vary: + - Accept-Encoding + Permissions-Policy: + - payment=(self "https://checkout.stripe.com" "https://connect-js.stripe.com" + "https://js.stripe.com" "https://*.js.stripe.com" "https://hooks.stripe.com") + Referrer-Policy: + - no-referrer, strict-origin-when-cross-origin + X-Content-Type-Options: + - nosniff + Server: + - cloudflare + Cf-Ray: + - 9a1148e8ce4eca3e-SJC + body: + encoding: ASCII-8BIT + string: "\n \n{\"id\":\"gen-1763571846-FETieOfNa9AWlT8DFqWa\",\"provider\":\"Google + AI Studio\",\"model\":\"google/gemini-2.0-flash-001\",\"object\":\"chat.completion\",\"created\":1763571846,\"choices\":[{\"logprobs\":null,\"finish_reason\":\"tool_calls\",\"native_finish_reason\":\"STOP\",\"index\":0,\"message\":{\"role\":\"assistant\",\"content\":\"\",\"refusal\":null,\"reasoning\":null,\"tool_calls\":[{\"index\":0,\"id\":\"tool_get_weather_Q8eSZXDe3h4Z1rQ84pb0\",\"type\":\"function\",\"function\":{\"name\":\"get_weather\",\"arguments\":\"{\\\"location\\\":\\\"unknown\\\"}\"}}]}}],\"usage\":{\"prompt_tokens\":15,\"completion_tokens\":5,\"total_tokens\":20,\"prompt_tokens_details\":{\"cached_tokens\":0},\"completion_tokens_details\":{\"reasoning_tokens\":0,\"image_tokens\":0}}}" + recorded_at: Wed, 19 Nov 2025 17:04:07 GMT +- request: + method: post + uri: https://openrouter.ai/api/v1/chat/completions + body: + encoding: UTF-8 + string: '{"model":"google/gemini-2.0-flash-001","messages":[{"role":"user","content":"What''s + the weather?"},{"role":"assistant","content":"","refusal":null,"tool_calls":[{"id":"tool_get_weather_Q8eSZXDe3h4Z1rQ84pb0","function":{"arguments":"{\"location\":\"unknown\"}","name":"get_weather"},"type":"function","index":0}],"reasoning":null},{"role":"tool","content":"{\"location\":\"unknown\",\"temperature\":\"72°F\",\"conditions\":\"sunny\"}","tool_call_id":"tool_get_weather_Q8eSZXDe3h4Z1rQ84pb0"}],"tools":[{"type":"function","function":{"name":"get_weather","description":"Get + weather","parameters":{"type":"object","properties":{"location":{"type":"string"}},"required":["location"]}}}],"tool_choice":null}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json + User-Agent: + - OpenAI::Client/Ruby 0.35.1 + Host: + - openrouter.ai + X-Stainless-Arch: + - arm64 + X-Stainless-Lang: + - ruby + X-Stainless-Os: + - MacOS + X-Stainless-Package-Version: + - 0.35.1 + X-Stainless-Runtime: + - ruby + X-Stainless-Runtime-Version: + - 3.4.7 + Content-Type: + - application/json + Authorization: + - Bearer ACCESS_TOKEN + Http-Referer: + - https://example.com + X-Title: + - Dummy + X-Stainless-Retry-Count: + - '0' + X-Stainless-Timeout: + - '600.0' + Content-Length: + - '702' + response: + status: + code: 200 + message: OK + headers: + Date: + - Wed, 19 Nov 2025 17:04:08 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Access-Control-Allow-Origin: + - "*" + Vary: + - Accept-Encoding + Permissions-Policy: + - payment=(self "https://checkout.stripe.com" "https://connect-js.stripe.com" + "https://js.stripe.com" "https://*.js.stripe.com" "https://hooks.stripe.com") + Referrer-Policy: + - no-referrer, strict-origin-when-cross-origin + X-Content-Type-Options: + - nosniff + Server: + - cloudflare + Cf-Ray: + - 9a1148ef4a17ebed-SJC + body: + encoding: ASCII-8BIT + string: !binary |- + CiAgICAgICAgIAp7ImlkIjoiZ2VuLTE3NjM1NzE4NDctTG01ZzJMV083NWxDdHlRbHlKMEoiLCJwcm92aWRlciI6Ikdvb2dsZSBBSSBTdHVkaW8iLCJtb2RlbCI6Imdvb2dsZS9nZW1pbmktMi4wLWZsYXNoLTAwMSIsIm9iamVjdCI6ImNoYXQuY29tcGxldGlvbiIsImNyZWF0ZWQiOjE3NjM1NzE4NDcsImNob2ljZXMiOlt7ImxvZ3Byb2JzIjpudWxsLCJmaW5pc2hfcmVhc29uIjoic3RvcCIsIm5hdGl2ZV9maW5pc2hfcmVhc29uIjoiU1RPUCIsImluZGV4IjowLCJtZXNzYWdlIjp7InJvbGUiOiJhc3Npc3RhbnQiLCJjb250ZW50IjoiVGhlIHdlYXRoZXIgaXMgc3Vubnkgd2l0aCBhIHRlbXBlcmF0dXJlIG9mIDcywrBGLiBUaGUgbG9jYXRpb24gaXMgdW5rbm93bi5cbiIsInJlZnVzYWwiOm51bGwsInJlYXNvbmluZyI6bnVsbH19XSwidXNhZ2UiOnsicHJvbXB0X3Rva2VucyI6NTIsImNvbXBsZXRpb25fdG9rZW5zIjoyMCwidG90YWxfdG9rZW5zIjo3MiwicHJvbXB0X3Rva2Vuc19kZXRhaWxzIjp7ImNhY2hlZF90b2tlbnMiOjB9LCJjb21wbGV0aW9uX3Rva2Vuc19kZXRhaWxzIjp7InJlYXNvbmluZ190b2tlbnMiOjAsImltYWdlX3Rva2VucyI6MH19fQ== + recorded_at: Wed, 19 Nov 2025 17:04:08 GMT +recorded_with: VCR 6.3.1 diff --git a/test/integration/open_router/common_format/tools_test.rb b/test/integration/open_router/common_format/tools_test.rb new file mode 100644 index 00000000..a06eb99c --- /dev/null +++ b/test/integration/open_router/common_format/tools_test.rb @@ -0,0 +1,367 @@ +# frozen_string_literal: true + +require_relative "../../test_helper" + +module Integration + module OpenRouter + module CommonFormat + class ToolsTest < ActiveSupport::TestCase + include Integration::TestHelper + + class TestAgent < ActiveAgent::Base + generate_with :openrouter, model: "google/gemini-2.0-flash-001" + + def get_weather(location:) + { location: location, temperature: "72°F", conditions: "sunny" } + end + + def calculate(operation:, a:, b:) + result = case operation + when "add" then a + b + when "subtract" then a - b + when "multiply" then a * b + when "divide" then a / b + end + { operation: operation, a: a, b: b, result: result } + end + + # Common format with 'parameters' key (recommended) + COMMON_FORMAT_PARAMETERS = { + model: "google/gemini-2.0-flash-001", + messages: [ + { + role: "user", + content: "What's the weather in San Francisco?" + } + ], + tools: [ + { + type: "function", + function: { + name: "get_weather", + description: "Get the current weather in a given location", + parameters: { + type: "object", + properties: { + location: { + type: "string", + description: "The city and state, e.g. San Francisco, CA" + } + }, + required: [ "location" ] + } + } + } + ] + } + def common_format_parameters + prompt( + message: "What's the weather in San Francisco?", + tools: [ + { + name: "get_weather", + description: "Get the current weather in a given location", + parameters: { + type: "object", + properties: { + location: { + type: "string", + description: "The city and state, e.g. San Francisco, CA" + } + }, + required: [ "location" ] + } + } + ] + ) + end + + # Common format with 'input_schema' key (should normalize to parameters) + COMMON_FORMAT_INPUT_SCHEMA = { + model: "google/gemini-2.0-flash-001", + messages: [ + { + role: "user", + content: "What's the weather in Boston?" + } + ], + tools: [ + { + type: "function", + function: { + name: "get_weather", + description: "Get the current weather in a given location", + parameters: { + type: "object", + properties: { + location: { + type: "string", + description: "The city and state, e.g. San Francisco, CA" + } + }, + required: [ "location" ] + } + } + } + ] + } + def common_format_input_schema + prompt( + message: "What's the weather in Boston?", + tools: [ + { + name: "get_weather", + description: "Get the current weather in a given location", + input_schema: { + type: "object", + properties: { + location: { + type: "string", + description: "The city and state, e.g. San Francisco, CA" + } + }, + required: [ "location" ] + } + } + ] + ) + end + + # Multiple tools in common format + COMMON_FORMAT_MULTIPLE_TOOLS = { + model: "google/gemini-2.0-flash-001", + messages: [ + { + role: "user", + content: "What's the weather in NYC and what's 5 plus 3?" + } + ], + tools: [ + { + type: "function", + function: { + name: "get_weather", + description: "Get the current weather", + parameters: { + type: "object", + properties: { + location: { type: "string" } + }, + required: [ "location" ] + } + } + }, + { + type: "function", + function: { + name: "calculate", + description: "Perform basic arithmetic", + parameters: { + type: "object", + properties: { + operation: { type: "string", enum: [ "add", "subtract", "multiply", "divide" ] }, + a: { type: "number" }, + b: { type: "number" } + }, + required: [ "operation", "a", "b" ] + } + } + } + ] + } + def common_format_multiple_tools + prompt( + message: "What's the weather in NYC and what's 5 plus 3?", + tools: [ + { + name: "get_weather", + description: "Get the current weather", + parameters: { + type: "object", + properties: { + location: { type: "string" } + }, + required: [ "location" ] + } + }, + { + name: "calculate", + description: "Perform basic arithmetic", + parameters: { + type: "object", + properties: { + operation: { type: "string", enum: [ "add", "subtract", "multiply", "divide" ] }, + a: { type: "number" }, + b: { type: "number" } + }, + required: [ "operation", "a", "b" ] + } + } + ] + ) + end + + # Tool choice - string format + COMMON_FORMAT_TOOL_CHOICE_AUTO = { + model: "google/gemini-2.0-flash-001", + messages: [ + { + role: "user", + content: "What's the weather?" + } + ], + tools: [ + { + type: "function", + function: { + name: "get_weather", + description: "Get weather", + parameters: { + type: "object", + properties: { + location: { type: "string" } + }, + required: [ "location" ] + } + } + } + ], + tool_choice: "auto" + } + def common_format_tool_choice_auto + prompt( + message: "What's the weather?", + tools: [ + { + name: "get_weather", + description: "Get weather", + parameters: { + type: "object", + properties: { + location: { type: "string" } + }, + required: [ "location" ] + } + } + ], + tool_choice: "auto" + ) + end + + # Tool choice - force tool use with "required" + COMMON_FORMAT_TOOL_CHOICE_REQUIRED = { + model: "google/gemini-2.0-flash-001", + messages: [ + { + role: "user", + content: "What's the weather?" + } + ], + tools: [ + { + type: "function", + function: { + name: "get_weather", + description: "Get weather", + parameters: { + type: "object", + properties: { + location: { type: "string" } + }, + required: [ "location" ] + } + } + } + ], + tool_choice: "any" + } + def common_format_tool_choice_required + prompt( + message: "What's the weather?", + tools: [ + { + name: "get_weather", + description: "Get weather", + parameters: { + type: "object", + properties: { + location: { type: "string" } + }, + required: [ "location" ] + } + } + ], + tool_choice: "required" + ) + end + + # Tool choice - specific tool + COMMON_FORMAT_TOOL_CHOICE_SPECIFIC = { + model: "google/gemini-2.0-flash-001", + messages: [ + { + role: "user", + content: "What's the weather?" + } + ], + tools: [ + { + type: "function", + function: { + name: "get_weather", + description: "Get weather", + parameters: { + type: "object", + properties: { + location: { type: "string" } + }, + required: [ "location" ] + } + } + } + ], + tool_choice: { + type: "function", + function: { + name: "get_weather" + } + } + } + def common_format_tool_choice_specific + prompt( + message: "What's the weather?", + tools: [ + { + name: "get_weather", + description: "Get weather", + parameters: { + type: "object", + properties: { + location: { type: "string" } + }, + required: [ "location" ] + } + } + ], + tool_choice: { name: "get_weather" } + ) + end + end + + ################################################################################ + # This automatically runs all the tests for the test actions + ################################################################################ + [ + :common_format_parameters, + :common_format_input_schema, + :common_format_multiple_tools, + :common_format_tool_choice_auto, + :common_format_tool_choice_required, + :common_format_tool_choice_specific + ].each do |action_name| + test_request_builder(TestAgent, action_name, :generate_now, TestAgent.const_get(action_name.to_s.upcase, true)) + end + end + end + end +end From 55893b1a39a1c70e3c914b2a703b8c4506c9170c Mon Sep 17 00:00:00 2001 From: Zane Wolfgang Pickett Date: Wed, 19 Nov 2025 09:23:33 -0800 Subject: [PATCH 05/17] Add Ollama Tools Common Format --- .../test_agent_common_format_input_schema.yml | 127 ++++++ ...est_agent_common_format_multiple_tools.yml | 144 +++++++ .../test_agent_common_format_parameters.yml | 133 +++++++ ...t_agent_common_format_tool_choice_auto.yml | 67 ++++ ...ent_common_format_tool_choice_required.yml | 68 ++++ ...ent_common_format_tool_choice_specific.yml | 69 ++++ .../ollama/chat/common_format/tools_test.rb | 369 ++++++++++++++++++ 7 files changed, 977 insertions(+) create mode 100644 test/fixtures/vcr_cassettes/integration/ollama/chat/common_format/tools_test/test_agent_common_format_input_schema.yml create mode 100644 test/fixtures/vcr_cassettes/integration/ollama/chat/common_format/tools_test/test_agent_common_format_multiple_tools.yml create mode 100644 test/fixtures/vcr_cassettes/integration/ollama/chat/common_format/tools_test/test_agent_common_format_parameters.yml create mode 100644 test/fixtures/vcr_cassettes/integration/ollama/chat/common_format/tools_test/test_agent_common_format_tool_choice_auto.yml create mode 100644 test/fixtures/vcr_cassettes/integration/ollama/chat/common_format/tools_test/test_agent_common_format_tool_choice_required.yml create mode 100644 test/fixtures/vcr_cassettes/integration/ollama/chat/common_format/tools_test/test_agent_common_format_tool_choice_specific.yml create mode 100644 test/integration/ollama/chat/common_format/tools_test.rb diff --git a/test/fixtures/vcr_cassettes/integration/ollama/chat/common_format/tools_test/test_agent_common_format_input_schema.yml b/test/fixtures/vcr_cassettes/integration/ollama/chat/common_format/tools_test/test_agent_common_format_input_schema.yml new file mode 100644 index 00000000..3434b2fe --- /dev/null +++ b/test/fixtures/vcr_cassettes/integration/ollama/chat/common_format/tools_test/test_agent_common_format_input_schema.yml @@ -0,0 +1,127 @@ +--- +http_interactions: +- request: + method: post + uri: http://127.0.0.1:11434/v1/chat/completions + body: + encoding: UTF-8 + string: '{"model":"qwen3:latest","messages":[{"role":"user","content":"What''s + the weather in Boston?"}],"tools":[{"type":"function","function":{"name":"get_weather","description":"Get + the current weather in a given location","parameters":{"type":"object","properties":{"location":{"type":"string","description":"The + city and state, e.g. San Francisco, CA"}},"required":["location"]}}}]}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json + User-Agent: + - OpenAI::Client/Ruby 0.35.1 + Host: + - 127.0.0.1:11434 + X-Stainless-Arch: + - arm64 + X-Stainless-Lang: + - ruby + X-Stainless-Os: + - MacOS + X-Stainless-Package-Version: + - 0.35.1 + X-Stainless-Runtime: + - ruby + X-Stainless-Runtime-Version: + - 3.4.7 + Content-Type: + - application/json + Authorization: + - Bearer ollama + X-Stainless-Retry-Count: + - '0' + X-Stainless-Timeout: + - '600.0' + Content-Length: + - '377' + response: + status: + code: 200 + message: OK + headers: + Content-Type: + - application/json + Date: + - Wed, 19 Nov 2025 17:20:35 GMT + Content-Length: + - '775' + body: + encoding: UTF-8 + string: '{"id":"chatcmpl-883","object":"chat.completion","created":1763572835,"model":"qwen3:latest","system_fingerprint":"fp_ollama","choices":[{"index":0,"message":{"role":"assistant","content":"","reasoning":"Okay, + the user is asking for the weather in Boston. Let me check the tools available. + There''s a get_weather function that requires a location parameter. The user + mentioned \"Boston,\" so I need to specify the location as Boston, MA to match + the format like San Francisco, CA. I''ll call the function with that parameter.\n","tool_calls":[{"id":"call_u167uvno","index":0,"type":"function","function":{"name":"get_weather","arguments":"{\"location\":\"Boston, + MA\"}"}}]},"finish_reason":"tool_calls"}],"usage":{"prompt_tokens":160,"completion_tokens":93,"total_tokens":253}} + + ' + recorded_at: Wed, 19 Nov 2025 17:20:35 GMT +- request: + method: post + uri: http://127.0.0.1:11434/v1/chat/completions + body: + encoding: UTF-8 + string: '{"model":"qwen3:latest","messages":[{"role":"user","content":"What''s + the weather in Boston?"},{"role":"assistant","content":"","tool_calls":[{"id":"call_u167uvno","function":{"arguments":"{\"location\":\"Boston, + MA\"}","name":"get_weather"},"type":"function","index":0}],"reasoning":"Okay, + the user is asking for the weather in Boston. Let me check the tools available. + There''s a get_weather function that requires a location parameter. The user + mentioned \"Boston,\" so I need to specify the location as Boston, MA to match + the format like San Francisco, CA. I''ll call the function with that parameter.\n"},{"role":"tool","content":"{\"location\":\"Boston, + MA\",\"temperature\":\"72°F\",\"conditions\":\"sunny\"}","tool_call_id":"call_u167uvno"}],"tools":[{"type":"function","function":{"name":"get_weather","description":"Get + the current weather in a given location","parameters":{"type":"object","properties":{"location":{"type":"string","description":"The + city and state, e.g. San Francisco, CA"}},"required":["location"]}}}]}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json + User-Agent: + - OpenAI::Client/Ruby 0.35.1 + Host: + - 127.0.0.1:11434 + X-Stainless-Arch: + - arm64 + X-Stainless-Lang: + - ruby + X-Stainless-Os: + - MacOS + X-Stainless-Package-Version: + - 0.35.1 + X-Stainless-Runtime: + - ruby + X-Stainless-Runtime-Version: + - 3.4.7 + Content-Type: + - application/json + Authorization: + - Bearer ollama + X-Stainless-Retry-Count: + - '0' + X-Stainless-Timeout: + - '600.0' + Content-Length: + - '1031' + response: + status: + code: 200 + message: OK + headers: + Content-Type: + - application/json + Date: + - Wed, 19 Nov 2025 17:20:39 GMT + Content-Length: + - '783' + body: + encoding: ASCII-8BIT + string: !binary |- + eyJpZCI6ImNoYXRjbXBsLTU4NSIsIm9iamVjdCI6ImNoYXQuY29tcGxldGlvbiIsImNyZWF0ZWQiOjE3NjM1NzI4MzksIm1vZGVsIjoicXdlbjM6bGF0ZXN0Iiwic3lzdGVtX2ZpbmdlcnByaW50IjoiZnBfb2xsYW1hIiwiY2hvaWNlcyI6W3siaW5kZXgiOjAsIm1lc3NhZ2UiOnsicm9sZSI6ImFzc2lzdGFudCIsImNvbnRlbnQiOiJUaGUgY3VycmVudCB3ZWF0aGVyIGluIEJvc3RvbiwgTUEgaXMgNzLCsEYgYW5kIHN1bm55LiBJdCBsb29rcyBsaWtlIGEgcGxlYXNhbnQgZGF5ISIsInJlYXNvbmluZyI6Ik9rYXksIHRoZSB1c2VyIGFza2VkIGZvciB0aGUgd2VhdGhlciBpbiBCb3N0b24uIEkgY2FsbGVkIHRoZSBnZXRfd2VhdGhlciBmdW5jdGlvbiB3aXRoIEJvc3RvbiwgTUEgYXMgdGhlIGxvY2F0aW9uLiBUaGUgcmVzcG9uc2UgY2FtZSBiYWNrIHdpdGggYSB0ZW1wZXJhdHVyZSBvZiA3MsKwRiBhbmQgc3VubnkgY29uZGl0aW9ucy4gTm93IEkgbmVlZCB0byBwcmVzZW50IHRoaXMgaW5mb3JtYXRpb24gY2xlYXJseS4gTGV0IG1lIG1ha2Ugc3VyZSB0byBtZW50aW9uIHRoZSB0ZW1wZXJhdHVyZSwgY29uZGl0aW9ucywgYW5kIG1heWJlIGFkZCBhIGZyaWVuZGx5IG5vdGUgYWJvdXQgdGhlIHdlYXRoZXIgYmVpbmcgcGxlYXNhbnQuIEtlZXAgaXQgY29uY2lzZSBhbmQgc3RyYWlnaHRmb3J3YXJkLlxuIn0sImZpbmlzaF9yZWFzb24iOiJzdG9wIn1dLCJ1c2FnZSI6eyJwcm9tcHRfdG9rZW5zIjoyODEsImNvbXBsZXRpb25fdG9rZW5zIjoxMDUsInRvdGFsX3Rva2VucyI6Mzg2fX0K + recorded_at: Wed, 19 Nov 2025 17:20:39 GMT +recorded_with: VCR 6.3.1 diff --git a/test/fixtures/vcr_cassettes/integration/ollama/chat/common_format/tools_test/test_agent_common_format_multiple_tools.yml b/test/fixtures/vcr_cassettes/integration/ollama/chat/common_format/tools_test/test_agent_common_format_multiple_tools.yml new file mode 100644 index 00000000..fa320216 --- /dev/null +++ b/test/fixtures/vcr_cassettes/integration/ollama/chat/common_format/tools_test/test_agent_common_format_multiple_tools.yml @@ -0,0 +1,144 @@ +--- +http_interactions: +- request: + method: post + uri: http://127.0.0.1:11434/v1/chat/completions + body: + encoding: UTF-8 + string: '{"model":"qwen3:latest","messages":[{"role":"user","content":"What''s + the weather in NYC and what''s 5 plus 3?"}],"tools":[{"type":"function","function":{"name":"get_weather","description":"Get + the current weather","parameters":{"type":"object","properties":{"location":{"type":"string"}},"required":["location"]}}},{"type":"function","function":{"name":"calculate","description":"Perform + basic arithmetic","parameters":{"type":"object","properties":{"operation":{"type":"string","enum":["add","subtract","multiply","divide"]},"a":{"type":"number"},"b":{"type":"number"}},"required":["operation","a","b"]}}}]}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json + User-Agent: + - OpenAI::Client/Ruby 0.35.1 + Host: + - 127.0.0.1:11434 + X-Stainless-Arch: + - arm64 + X-Stainless-Lang: + - ruby + X-Stainless-Os: + - MacOS + X-Stainless-Package-Version: + - 0.35.1 + X-Stainless-Runtime: + - ruby + X-Stainless-Runtime-Version: + - 3.4.7 + Content-Type: + - application/json + Authorization: + - Bearer ollama + X-Stainless-Retry-Count: + - '0' + X-Stainless-Timeout: + - '600.0' + Content-Length: + - '607' + response: + status: + code: 200 + message: OK + headers: + Content-Type: + - application/json + Date: + - Wed, 19 Nov 2025 17:21:09 GMT + Content-Length: + - '1602' + body: + encoding: UTF-8 + string: '{"id":"chatcmpl-547","object":"chat.completion","created":1763572869,"model":"qwen3:latest","system_fingerprint":"fp_ollama","choices":[{"index":0,"message":{"role":"assistant","content":"","reasoning":"Okay, + let''s tackle this user query. The user is asking two things: the weather + in NYC and the result of 5 plus 3. \n\nFirst, I need to check which functions + are available. There''s get_weather for the weather info and calculate for + arithmetic operations. \n\nFor the first part, \"What''s the weather in NYC?\", + I should call get_weather with the location parameter set to \"NYC\". That + should retrieve the current weather data.\n\nNext, the second part is \"what''s + 5 plus 3?\" Here, the operation is addition. The calculate function requires + operation, a, and b. So operation is \"add\", a is 5, and b is 3. I''ll need + to make sure the parameters are correctly formatted as numbers.\n\nI need + to make two separate tool calls. First for get_weather with location NYC, + then for calculate with add, 5, and 3. Let me structure each tool call properly + in JSON within the XML tags. Double-checking the parameters to ensure they + match the required types and enums. No mistakes there. Alright, ready to output + the tool calls.\n","tool_calls":[{"id":"call_nwftoaxi","index":0,"type":"function","function":{"name":"get_weather","arguments":"{\"location\":\"NYC\"}"}},{"id":"call_vialglfv","index":1,"type":"function","function":{"name":"calculate","arguments":"{\"a\":5,\"b\":3,\"operation\":\"add\"}"}}]},"finish_reason":"tool_calls"}],"usage":{"prompt_tokens":215,"completion_tokens":285,"total_tokens":500}} + + ' + recorded_at: Wed, 19 Nov 2025 17:21:09 GMT +- request: + method: post + uri: http://127.0.0.1:11434/v1/chat/completions + body: + encoding: UTF-8 + string: '{"model":"qwen3:latest","messages":[{"role":"user","content":"What''s + the weather in NYC and what''s 5 plus 3?"},{"role":"assistant","content":"","tool_calls":[{"id":"call_nwftoaxi","function":{"arguments":"{\"location\":\"NYC\"}","name":"get_weather"},"type":"function","index":0},{"id":"call_vialglfv","function":{"arguments":"{\"a\":5,\"b\":3,\"operation\":\"add\"}","name":"calculate"},"type":"function","index":1}],"reasoning":"Okay, + let''s tackle this user query. The user is asking two things: the weather + in NYC and the result of 5 plus 3. \n\nFirst, I need to check which functions + are available. There''s get_weather for the weather info and calculate for + arithmetic operations. \n\nFor the first part, \"What''s the weather in NYC?\", + I should call get_weather with the location parameter set to \"NYC\". That + should retrieve the current weather data.\n\nNext, the second part is \"what''s + 5 plus 3?\" Here, the operation is addition. The calculate function requires + operation, a, and b. So operation is \"add\", a is 5, and b is 3. I''ll need + to make sure the parameters are correctly formatted as numbers.\n\nI need + to make two separate tool calls. First for get_weather with location NYC, + then for calculate with add, 5, and 3. Let me structure each tool call properly + in JSON within the XML tags. Double-checking the parameters to ensure they + match the required types and enums. No mistakes there. Alright, ready to output + the tool calls.\n"},{"role":"tool","content":"{\"location\":\"NYC\",\"temperature\":\"72°F\",\"conditions\":\"sunny\"}{\"operation\":\"add\",\"a\":5,\"b\":3,\"result\":8}","tool_call_id":"call_nwftoaxi"}],"tools":[{"type":"function","function":{"name":"get_weather","description":"Get + the current weather","parameters":{"type":"object","properties":{"location":{"type":"string"}},"required":["location"]}}},{"type":"function","function":{"name":"calculate","description":"Perform + basic arithmetic","parameters":{"type":"object","properties":{"operation":{"type":"string","enum":["add","subtract","multiply","divide"]},"a":{"type":"number"},"b":{"type":"number"}},"required":["operation","a","b"]}}}]}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json + User-Agent: + - OpenAI::Client/Ruby 0.35.1 + Host: + - 127.0.0.1:11434 + X-Stainless-Arch: + - arm64 + X-Stainless-Lang: + - ruby + X-Stainless-Os: + - MacOS + X-Stainless-Package-Version: + - 0.35.1 + X-Stainless-Runtime: + - ruby + X-Stainless-Runtime-Version: + - 3.4.7 + Content-Type: + - application/json + Authorization: + - Bearer ollama + X-Stainless-Retry-Count: + - '0' + X-Stainless-Timeout: + - '600.0' + Content-Length: + - '2132' + response: + status: + code: 200 + message: OK + headers: + Content-Type: + - application/json + Date: + - Wed, 19 Nov 2025 17:21:16 GMT + Content-Length: + - '895' + body: + encoding: ASCII-8BIT + string: !binary |- + eyJpZCI6ImNoYXRjbXBsLTg0NyIsIm9iamVjdCI6ImNoYXQuY29tcGxldGlvbiIsImNyZWF0ZWQiOjE3NjM1NzI4NzYsIm1vZGVsIjoicXdlbjM6bGF0ZXN0Iiwic3lzdGVtX2ZpbmdlcnByaW50IjoiZnBfb2xsYW1hIiwiY2hvaWNlcyI6W3siaW5kZXgiOjAsIm1lc3NhZ2UiOnsicm9sZSI6ImFzc2lzdGFudCIsImNvbnRlbnQiOiJUaGUgY3VycmVudCB3ZWF0aGVyIGluIE5ZQyBpcyA3MsKwRiBhbmQgc3VubnkuIFxuXG41IHBsdXMgMyBlcXVhbHMgOC4iLCJyZWFzb25pbmciOiJPa2F5LCBsZXQncyBzZWUuIFRoZSB1c2VyIGFza2VkIGZvciB0d28gdGhpbmdzOiB0aGUgd2VhdGhlciBpbiBOWUMgYW5kIHRoZSBzdW0gb2YgNSBhbmQgMy4gRmlyc3QsIEkgbmVlZCB0byBwcm9jZXNzIHRoZSB3ZWF0aGVyIGRhdGEuIFRoZSB0b29sIHJlc3BvbnNlIHNheXMgaXQncyA3MsKwRiBhbmQgc3VubnkgaW4gTllDLiBHb3QgdGhhdC4gVGhlbiwgdGhlIGNhbGN1bGF0aW9uIHBhcnQ6IDUgcGx1cyAzIGVxdWFscyA4LiBUaGUgdG9vbCByZXNwb25zZSBjb25maXJtcyB0aGUgcmVzdWx0IGlzIDguIE5vdywgSSBzaG91bGQgcHJlc2VudCBib3RoIGFuc3dlcnMgY2xlYXJseS4gTGV0IG1lIHN0YXJ0IHdpdGggdGhlIHdlYXRoZXIsIG1lbnRpb24gdGhlIHRlbXBlcmF0dXJlIGFuZCBjb25kaXRpb25zLCB0aGVuIHN0YXRlIHRoZSBtYXRoIHJlc3VsdC4gTWFrZSBzdXJlIHRoZSByZXNwb25zZSBpcyBmcmllbmRseSBhbmQgc3RyYWlnaHRmb3J3YXJkLiBBbHJpZ2h0LCBwdXR0aW5nIGl0IGFsbCB0b2dldGhlci5cbiJ9LCJmaW5pc2hfcmVhc29uIjoic3RvcCJ9XSwidXNhZ2UiOnsicHJvbXB0X3Rva2VucyI6NTM1LCJjb21wbGV0aW9uX3Rva2VucyI6MTUxLCJ0b3RhbF90b2tlbnMiOjY4Nn19Cg== + recorded_at: Wed, 19 Nov 2025 17:21:16 GMT +recorded_with: VCR 6.3.1 diff --git a/test/fixtures/vcr_cassettes/integration/ollama/chat/common_format/tools_test/test_agent_common_format_parameters.yml b/test/fixtures/vcr_cassettes/integration/ollama/chat/common_format/tools_test/test_agent_common_format_parameters.yml new file mode 100644 index 00000000..aa284f3b --- /dev/null +++ b/test/fixtures/vcr_cassettes/integration/ollama/chat/common_format/tools_test/test_agent_common_format_parameters.yml @@ -0,0 +1,133 @@ +--- +http_interactions: +- request: + method: post + uri: http://127.0.0.1:11434/v1/chat/completions + body: + encoding: UTF-8 + string: '{"model":"qwen3:latest","messages":[{"role":"user","content":"What''s + the weather in San Francisco?"}],"tools":[{"type":"function","function":{"name":"get_weather","description":"Get + the current weather in a given location","parameters":{"type":"object","properties":{"location":{"type":"string","description":"The + city and state, e.g. San Francisco, CA"}},"required":["location"]}}}]}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json + User-Agent: + - OpenAI::Client/Ruby 0.35.1 + Host: + - 127.0.0.1:11434 + X-Stainless-Arch: + - arm64 + X-Stainless-Lang: + - ruby + X-Stainless-Os: + - MacOS + X-Stainless-Package-Version: + - 0.35.1 + X-Stainless-Runtime: + - ruby + X-Stainless-Runtime-Version: + - 3.4.7 + Content-Type: + - application/json + Authorization: + - Bearer ollama + X-Stainless-Retry-Count: + - '0' + X-Stainless-Timeout: + - '600.0' + Content-Length: + - '384' + response: + status: + code: 200 + message: OK + headers: + Content-Type: + - application/json + Date: + - Wed, 19 Nov 2025 17:20:44 GMT + Content-Length: + - '993' + body: + encoding: UTF-8 + string: '{"id":"chatcmpl-837","object":"chat.completion","created":1763572844,"model":"qwen3:latest","system_fingerprint":"fp_ollama","choices":[{"index":0,"message":{"role":"assistant","content":"","reasoning":"Okay, + the user is asking for the weather in San Francisco. Let me check the tools + provided. There''s a function called get_weather that takes a location parameter. + The required parameter is location, which should be a string like \"San Francisco, + CA\". The user just said \"San Francisco\", so I should format that into the + required format. Let me make sure to include the state abbreviation. The function + doesn''t have any other parameters, so I just need to pass \"San Francisco, + CA\" as the location. Alright, that should do it.\n","tool_calls":[{"id":"call_1mebjeab","index":0,"type":"function","function":{"name":"get_weather","arguments":"{\"location\":\"San + Francisco, CA\"}"}}]},"finish_reason":"tool_calls"}],"usage":{"prompt_tokens":161,"completion_tokens":138,"total_tokens":299}} + + ' + recorded_at: Wed, 19 Nov 2025 17:20:44 GMT +- request: + method: post + uri: http://127.0.0.1:11434/v1/chat/completions + body: + encoding: UTF-8 + string: '{"model":"qwen3:latest","messages":[{"role":"user","content":"What''s + the weather in San Francisco?"},{"role":"assistant","content":"","tool_calls":[{"id":"call_1mebjeab","function":{"arguments":"{\"location\":\"San + Francisco, CA\"}","name":"get_weather"},"type":"function","index":0}],"reasoning":"Okay, + the user is asking for the weather in San Francisco. Let me check the tools + provided. There''s a function called get_weather that takes a location parameter. + The required parameter is location, which should be a string like \"San Francisco, + CA\". The user just said \"San Francisco\", so I should format that into the + required format. Let me make sure to include the state abbreviation. The function + doesn''t have any other parameters, so I just need to pass \"San Francisco, + CA\" as the location. Alright, that should do it.\n"},{"role":"tool","content":"{\"location\":\"San + Francisco, CA\",\"temperature\":\"72°F\",\"conditions\":\"sunny\"}","tool_call_id":"call_1mebjeab"}],"tools":[{"type":"function","function":{"name":"get_weather","description":"Get + the current weather in a given location","parameters":{"type":"object","properties":{"location":{"type":"string","description":"The + city and state, e.g. San Francisco, CA"}},"required":["location"]}}}]}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json + User-Agent: + - OpenAI::Client/Ruby 0.35.1 + Host: + - 127.0.0.1:11434 + X-Stainless-Arch: + - arm64 + X-Stainless-Lang: + - ruby + X-Stainless-Os: + - MacOS + X-Stainless-Package-Version: + - 0.35.1 + X-Stainless-Runtime: + - ruby + X-Stainless-Runtime-Version: + - 3.4.7 + Content-Type: + - application/json + Authorization: + - Bearer ollama + X-Stainless-Retry-Count: + - '0' + X-Stainless-Timeout: + - '600.0' + Content-Length: + - '1262' + response: + status: + code: 200 + message: OK + headers: + Content-Type: + - application/json + Date: + - Wed, 19 Nov 2025 17:20:49 GMT + Content-Length: + - '988' + body: + encoding: ASCII-8BIT + string: !binary |- + eyJpZCI6ImNoYXRjbXBsLTY0MSIsIm9iamVjdCI6ImNoYXQuY29tcGxldGlvbiIsImNyZWF0ZWQiOjE3NjM1NzI4NDksIm1vZGVsIjoicXdlbjM6bGF0ZXN0Iiwic3lzdGVtX2ZpbmdlcnByaW50IjoiZnBfb2xsYW1hIiwiY2hvaWNlcyI6W3siaW5kZXgiOjAsIm1lc3NhZ2UiOnsicm9sZSI6ImFzc2lzdGFudCIsImNvbnRlbnQiOiJUaGUgY3VycmVudCB3ZWF0aGVyIGluIFNhbiBGcmFuY2lzY28sIENBIGlzICoqNzLCsEYqKiBhbmQgKipzdW5ueSoqLiBJdCBsb29rcyBsaWtlIGEgcGxlYXNhbnQgZGF5IHdpdGggY2xlYXIgc2tpZXMhIExldCBtZSBrbm93IGlmIHlvdSdkIGxpa2UgYWRkaXRpb25hbCBkZXRhaWxzLiDwn4yeIiwicmVhc29uaW5nIjoiT2theSwgdGhlIHVzZXIgYXNrZWQgZm9yIHRoZSB3ZWF0aGVyIGluIFNhbiBGcmFuY2lzY28uIEkgY2FsbGVkIHRoZSBnZXRfd2VhdGhlciBmdW5jdGlvbiB3aXRoIFwiU2FuIEZyYW5jaXNjbywgQ0FcIiBhcyB0aGUgbG9jYXRpb24uIFRoZSByZXNwb25zZSBjYW1lIGJhY2sgd2l0aCBhIHRlbXBlcmF0dXJlIG9mIDcywrBGIGFuZCBzdW5ueSBjb25kaXRpb25zLiBOb3cgSSBuZWVkIHRvIHByZXNlbnQgdGhpcyBpbmZvcm1hdGlvbiBjbGVhcmx5LiBMZXQgbWUgc3RhcnQgYnkgc3RhdGluZyB0aGUgY3VycmVudCB3ZWF0aGVyLCBtZW50aW9uIHRoZSB0ZW1wZXJhdHVyZSBhbmQgY29uZGl0aW9ucy4gS2VlcCBpdCBzaW1wbGUgYW5kIGZyaWVuZGx5LiBNYXliZSBhZGQgYSBub3RlIGFib3V0IHRoZSB3ZWF0aGVyIGJlaW5nIHBsZWFzYW50LiBNYWtlIHN1cmUgdGhlIHVzZXIga25vd3MgdGhleSBjYW4gYXNrIGZvciBtb3JlIGRldGFpbHMgaWYgbmVlZGVkLiBBbHJpZ2h0LCB0aGF0IHNob3VsZCBjb3ZlciBpdC5cbiJ9LCJmaW5pc2hfcmVhc29uIjoic3RvcCJ9XSwidXNhZ2UiOnsicHJvbXB0X3Rva2VucyI6MzI4LCJjb21wbGV0aW9uX3Rva2VucyI6MTU0LCJ0b3RhbF90b2tlbnMiOjQ4Mn19Cg== + recorded_at: Wed, 19 Nov 2025 17:20:49 GMT +recorded_with: VCR 6.3.1 diff --git a/test/fixtures/vcr_cassettes/integration/ollama/chat/common_format/tools_test/test_agent_common_format_tool_choice_auto.yml b/test/fixtures/vcr_cassettes/integration/ollama/chat/common_format/tools_test/test_agent_common_format_tool_choice_auto.yml new file mode 100644 index 00000000..89e45428 --- /dev/null +++ b/test/fixtures/vcr_cassettes/integration/ollama/chat/common_format/tools_test/test_agent_common_format_tool_choice_auto.yml @@ -0,0 +1,67 @@ +--- +http_interactions: +- request: + method: post + uri: http://127.0.0.1:11434/v1/chat/completions + body: + encoding: UTF-8 + string: '{"model":"qwen3:latest","messages":[{"role":"user","content":"What''s + the weather?"}],"tools":[{"type":"function","function":{"name":"get_weather","description":"Get + weather","parameters":{"type":"object","properties":{"location":{"type":"string"}},"required":["location"]}}}],"tool_choice":"auto"}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json + User-Agent: + - OpenAI::Client/Ruby 0.35.1 + Host: + - 127.0.0.1:11434 + X-Stainless-Arch: + - arm64 + X-Stainless-Lang: + - ruby + X-Stainless-Os: + - MacOS + X-Stainless-Package-Version: + - 0.35.1 + X-Stainless-Runtime: + - ruby + X-Stainless-Runtime-Version: + - 3.4.7 + Content-Type: + - application/json + Authorization: + - Bearer ollama + X-Stainless-Retry-Count: + - '0' + X-Stainless-Timeout: + - '600.0' + Content-Length: + - '297' + response: + status: + code: 200 + message: OK + headers: + Content-Type: + - application/json + Date: + - Wed, 19 Nov 2025 17:20:59 GMT + Content-Length: + - '858' + body: + encoding: UTF-8 + string: '{"id":"chatcmpl-691","object":"chat.completion","created":1763572859,"model":"qwen3:latest","system_fingerprint":"fp_ollama","choices":[{"index":0,"message":{"role":"assistant","content":"Could + you please specify the location you''d like the weather information for?","reasoning":"Okay, + the user asked, \"What''s the weather?\" but didn''t specify the location. + I need to call the get_weather function, which requires a location parameter. + Since the user didn''t provide one, I should ask them to clarify where they + want the weather information for. Let me check the function details again + to confirm the parameters. The function requires ''location'' as a string. + Alright, so I can''t proceed without that info. Time to prompt the user for + the missing details.\n"},"finish_reason":"stop"}],"usage":{"prompt_tokens":137,"completion_tokens":118,"total_tokens":255}} + + ' + recorded_at: Wed, 19 Nov 2025 17:20:59 GMT +recorded_with: VCR 6.3.1 diff --git a/test/fixtures/vcr_cassettes/integration/ollama/chat/common_format/tools_test/test_agent_common_format_tool_choice_required.yml b/test/fixtures/vcr_cassettes/integration/ollama/chat/common_format/tools_test/test_agent_common_format_tool_choice_required.yml new file mode 100644 index 00000000..0793738c --- /dev/null +++ b/test/fixtures/vcr_cassettes/integration/ollama/chat/common_format/tools_test/test_agent_common_format_tool_choice_required.yml @@ -0,0 +1,68 @@ +--- +http_interactions: +- request: + method: post + uri: http://127.0.0.1:11434/v1/chat/completions + body: + encoding: UTF-8 + string: '{"model":"qwen3:latest","messages":[{"role":"user","content":"What''s + the weather?"}],"tools":[{"type":"function","function":{"name":"get_weather","description":"Get + weather","parameters":{"type":"object","properties":{"location":{"type":"string"}},"required":["location"]}}}],"tool_choice":"required"}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json + User-Agent: + - OpenAI::Client/Ruby 0.35.1 + Host: + - 127.0.0.1:11434 + X-Stainless-Arch: + - arm64 + X-Stainless-Lang: + - ruby + X-Stainless-Os: + - MacOS + X-Stainless-Package-Version: + - 0.35.1 + X-Stainless-Runtime: + - ruby + X-Stainless-Runtime-Version: + - 3.4.7 + Content-Type: + - application/json + Authorization: + - Bearer ollama + X-Stainless-Retry-Count: + - '0' + X-Stainless-Timeout: + - '600.0' + Content-Length: + - '301' + response: + status: + code: 200 + message: OK + headers: + Content-Type: + - application/json + Date: + - Wed, 19 Nov 2025 17:20:31 GMT + Content-Length: + - '988' + body: + encoding: UTF-8 + string: '{"id":"chatcmpl-615","object":"chat.completion","created":1763572831,"model":"qwen3:latest","system_fingerprint":"fp_ollama","choices":[{"index":0,"message":{"role":"assistant","content":"I + need to know the location to check the weather. Could you please specify the + city or region you''re interested in?","reasoning":"Okay, the user asked, + \"What''s the weather?\" I need to figure out which function to use. The available + tool is get_weather, which requires a location parameter. But the user didn''t + specify a location. Hmm, maybe I should ask them for the location. Wait, the + function requires it, so I can''t proceed without that info. Let me check + the function details again. Yes, location is a required parameter. Since the + user didn''t mention where, I should prompt them to provide the location so + I can fetch the weather data. Alright, time to respond by asking for the location.\n"},"finish_reason":"stop"}],"usage":{"prompt_tokens":137,"completion_tokens":151,"total_tokens":288}} + + ' + recorded_at: Wed, 19 Nov 2025 17:20:31 GMT +recorded_with: VCR 6.3.1 diff --git a/test/fixtures/vcr_cassettes/integration/ollama/chat/common_format/tools_test/test_agent_common_format_tool_choice_specific.yml b/test/fixtures/vcr_cassettes/integration/ollama/chat/common_format/tools_test/test_agent_common_format_tool_choice_specific.yml new file mode 100644 index 00000000..48d9cd7a --- /dev/null +++ b/test/fixtures/vcr_cassettes/integration/ollama/chat/common_format/tools_test/test_agent_common_format_tool_choice_specific.yml @@ -0,0 +1,69 @@ +--- +http_interactions: +- request: + method: post + uri: http://127.0.0.1:11434/v1/chat/completions + body: + encoding: UTF-8 + string: '{"model":"qwen3:latest","messages":[{"role":"user","content":"What''s + the weather?"}],"tools":[{"type":"function","function":{"name":"get_weather","description":"Get + weather","parameters":{"type":"object","properties":{"location":{"type":"string"}},"required":["location"]}}}],"tool_choice":{"type":"function","function":{"name":"get_weather"}}}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json + User-Agent: + - OpenAI::Client/Ruby 0.35.1 + Host: + - 127.0.0.1:11434 + X-Stainless-Arch: + - arm64 + X-Stainless-Lang: + - ruby + X-Stainless-Os: + - MacOS + X-Stainless-Package-Version: + - 0.35.1 + X-Stainless-Runtime: + - ruby + X-Stainless-Runtime-Version: + - 3.4.7 + Content-Type: + - application/json + Authorization: + - Bearer ollama + X-Stainless-Retry-Count: + - '0' + X-Stainless-Timeout: + - '600.0' + Content-Length: + - '344' + response: + status: + code: 200 + message: OK + headers: + Content-Type: + - application/json + Date: + - Wed, 19 Nov 2025 17:20:55 GMT + Content-Length: + - '1012' + body: + encoding: UTF-8 + string: '{"id":"chatcmpl-675","object":"chat.completion","created":1763572855,"model":"qwen3:latest","system_fingerprint":"fp_ollama","choices":[{"index":0,"message":{"role":"assistant","content":"I + need to know the location to check the weather. Could you please specify the + city or region you''re interested in?","reasoning":"Okay, the user asked, + \"What''s the weather?\" I need to figure out which function to use. The available + tool is get_weather, which requires a location parameter. But the user didn''t + specify a location. Hmm, maybe I should ask them for the location. Wait, the + function''s parameters are required to have \"location\", so I can''t call + it without that info. Since the user didn''t provide it, I should prompt them + to specify the location so I can retrieve the weather. Let me check the function + again to confirm. Yep, location is required. Alright, time to ask the user + for the location details.\n"},"finish_reason":"stop"}],"usage":{"prompt_tokens":137,"completion_tokens":157,"total_tokens":294}} + + ' + recorded_at: Wed, 19 Nov 2025 17:20:55 GMT +recorded_with: VCR 6.3.1 diff --git a/test/integration/ollama/chat/common_format/tools_test.rb b/test/integration/ollama/chat/common_format/tools_test.rb new file mode 100644 index 00000000..f5fa4d46 --- /dev/null +++ b/test/integration/ollama/chat/common_format/tools_test.rb @@ -0,0 +1,369 @@ +# frozen_string_literal: true + +require_relative "../../../test_helper" + +module Integration + module Ollama + module Chat + module CommonFormat + class ToolsTest < ActiveSupport::TestCase + include Integration::TestHelper + + class TestAgent < ActiveAgent::Base + generate_with :ollama, model: "qwen3:latest" + + def get_weather(location:) + { location: location, temperature: "72°F", conditions: "sunny" } + end + + def calculate(operation:, a:, b:) + result = case operation + when "add" then a + b + when "subtract" then a - b + when "multiply" then a * b + when "divide" then a / b + end + { operation: operation, a: a, b: b, result: result } + end + + # Common format with 'parameters' key (recommended) + COMMON_FORMAT_PARAMETERS = { + model: "qwen3:latest", + messages: [ + { + role: "user", + content: "What's the weather in San Francisco?" + } + ], + tools: [ + { + type: "function", + function: { + name: "get_weather", + description: "Get the current weather in a given location", + parameters: { + type: "object", + properties: { + location: { + type: "string", + description: "The city and state, e.g. San Francisco, CA" + } + }, + required: [ "location" ] + } + } + } + ] + } + def common_format_parameters + prompt( + message: "What's the weather in San Francisco?", + tools: [ + { + name: "get_weather", + description: "Get the current weather in a given location", + parameters: { + type: "object", + properties: { + location: { + type: "string", + description: "The city and state, e.g. San Francisco, CA" + } + }, + required: [ "location" ] + } + } + ] + ) + end + + # Common format with 'input_schema' key (should normalize to parameters) + COMMON_FORMAT_INPUT_SCHEMA = { + model: "qwen3:latest", + messages: [ + { + role: "user", + content: "What's the weather in Boston?" + } + ], + tools: [ + { + type: "function", + function: { + name: "get_weather", + description: "Get the current weather in a given location", + parameters: { + type: "object", + properties: { + location: { + type: "string", + description: "The city and state, e.g. San Francisco, CA" + } + }, + required: [ "location" ] + } + } + } + ] + } + def common_format_input_schema + prompt( + message: "What's the weather in Boston?", + tools: [ + { + name: "get_weather", + description: "Get the current weather in a given location", + input_schema: { + type: "object", + properties: { + location: { + type: "string", + description: "The city and state, e.g. San Francisco, CA" + } + }, + required: [ "location" ] + } + } + ] + ) + end + + # Multiple tools in common format + COMMON_FORMAT_MULTIPLE_TOOLS = { + model: "qwen3:latest", + messages: [ + { + role: "user", + content: "What's the weather in NYC and what's 5 plus 3?" + } + ], + tools: [ + { + type: "function", + function: { + name: "get_weather", + description: "Get the current weather", + parameters: { + type: "object", + properties: { + location: { type: "string" } + }, + required: [ "location" ] + } + } + }, + { + type: "function", + function: { + name: "calculate", + description: "Perform basic arithmetic", + parameters: { + type: "object", + properties: { + operation: { type: "string", enum: [ "add", "subtract", "multiply", "divide" ] }, + a: { type: "number" }, + b: { type: "number" } + }, + required: [ "operation", "a", "b" ] + } + } + } + ] + } + def common_format_multiple_tools + prompt( + message: "What's the weather in NYC and what's 5 plus 3?", + tools: [ + { + name: "get_weather", + description: "Get the current weather", + parameters: { + type: "object", + properties: { + location: { type: "string" } + }, + required: [ "location" ] + } + }, + { + name: "calculate", + description: "Perform basic arithmetic", + parameters: { + type: "object", + properties: { + operation: { type: "string", enum: [ "add", "subtract", "multiply", "divide" ] }, + a: { type: "number" }, + b: { type: "number" } + }, + required: [ "operation", "a", "b" ] + } + } + ] + ) + end + + # Tool choice - string format + COMMON_FORMAT_TOOL_CHOICE_AUTO = { + model: "qwen3:latest", + messages: [ + { + role: "user", + content: "What's the weather?" + } + ], + tools: [ + { + type: "function", + function: { + name: "get_weather", + description: "Get weather", + parameters: { + type: "object", + properties: { + location: { type: "string" } + }, + required: [ "location" ] + } + } + } + ], + tool_choice: "auto" + } + def common_format_tool_choice_auto + prompt( + message: "What's the weather?", + tools: [ + { + name: "get_weather", + description: "Get weather", + parameters: { + type: "object", + properties: { + location: { type: "string" } + }, + required: [ "location" ] + } + } + ], + tool_choice: "auto" + ) + end + + # Tool choice - force tool use with "required" + COMMON_FORMAT_TOOL_CHOICE_REQUIRED = { + model: "qwen3:latest", + messages: [ + { + role: "user", + content: "What's the weather?" + } + ], + tools: [ + { + type: "function", + function: { + name: "get_weather", + description: "Get weather", + parameters: { + type: "object", + properties: { + location: { type: "string" } + }, + required: [ "location" ] + } + } + } + ], + tool_choice: "required" + } + def common_format_tool_choice_required + prompt( + message: "What's the weather?", + tools: [ + { + name: "get_weather", + description: "Get weather", + parameters: { + type: "object", + properties: { + location: { type: "string" } + }, + required: [ "location" ] + } + } + ], + tool_choice: "required" + ) + end + + # Tool choice - specific tool + COMMON_FORMAT_TOOL_CHOICE_SPECIFIC = { + model: "qwen3:latest", + messages: [ + { + role: "user", + content: "What's the weather?" + } + ], + tools: [ + { + type: "function", + function: { + name: "get_weather", + description: "Get weather", + parameters: { + type: "object", + properties: { + location: { type: "string" } + }, + required: [ "location" ] + } + } + } + ], + tool_choice: { + type: "function", + function: { + name: "get_weather" + } + } + } + def common_format_tool_choice_specific + prompt( + message: "What's the weather?", + tools: [ + { + name: "get_weather", + description: "Get weather", + parameters: { + type: "object", + properties: { + location: { type: "string" } + }, + required: [ "location" ] + } + } + ], + tool_choice: { name: "get_weather" } + ) + end + end + + ################################################################################ + # This automatically runs all the tests for the test actions + ################################################################################ + [ + :common_format_parameters, + :common_format_input_schema, + :common_format_multiple_tools, + :common_format_tool_choice_auto, + :common_format_tool_choice_required, + :common_format_tool_choice_specific + ].each do |action_name| + test_request_builder(TestAgent, action_name, :generate_now, TestAgent.const_get(action_name.to_s.upcase, true)) + end + end + end + end + end +end From 7f4ece3bcf90cff8ad3ab889e63b3863d032fe16 Mon Sep 17 00:00:00 2001 From: Zane Wolfgang Pickett Date: Wed, 19 Nov 2025 09:30:37 -0800 Subject: [PATCH 06/17] Update Tools Previewing --- .../providers/concerns/previewable.rb | 44 ++++++++++++++++--- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/lib/active_agent/providers/concerns/previewable.rb b/lib/active_agent/providers/concerns/previewable.rb index dcdf81d7..bef818f8 100644 --- a/lib/active_agent/providers/concerns/previewable.rb +++ b/lib/active_agent/providers/concerns/previewable.rb @@ -86,7 +86,13 @@ def render_single_message(message, index) "### Message #{index} (#{role.capitalize})\n#{content}" end - # Renders available tools with descriptions and parameter schemas. + # Renders tools section for preview. + # + # Handles multiple tool formats: + # - Common format: {name: "...", description: "...", parameters: {...}} + # - Anthropic format: {name: "...", description: "...", input_schema: {...}} + # - Chat API format: {type: "function", function: {name: "...", description: "...", parameters: {...}}} + # - Responses API format: {type: "function", name: "...", description: "...", parameters: {...}} # # @param tools [Array] # @return [String] @@ -96,17 +102,45 @@ def render_tools_section(tools) content = +"## Tools\n\n" tools.each_with_index do |tool, index| - content << "### #{tool[:name] || "Tool #{index + 1}"}\n" - content << "**Description:** #{tool[:description] || 'No description'}\n\n" + # Extract name and description from different formats + tool_name, tool_description, tool_params = extract_tool_details(tool) + + content << "### #{tool_name || "Tool #{index + 1}"}\n" + content << "**Description:** #{tool_description || 'No description'}\n\n" - if tool[:parameters] - content << "**Parameters:**\n```json\n#{JSON.pretty_generate(tool[:parameters])}\n```\n\n" + if tool_params + content << "**Parameters:**\n```json\n#{JSON.pretty_generate(tool_params)}\n```\n\n" end end content.chomp end + # Extracts tool details from different formats. + # + # @param tool [Hash] + # @return [Array] [name, description, parameters] + def extract_tool_details(tool) + tool_hash = tool.is_a?(Hash) ? tool : {} + + # Chat API nested format: {type: "function", function: {...}} + if tool_hash[:type] == "function" && tool_hash[:function] + func = tool_hash[:function] + return [ + func[:name], + func[:description], + func[:parameters] || func[:input_schema] + ] + end + + # Flat formats (common, Anthropic, Responses) + [ + tool_hash[:name], + tool_hash[:description], + tool_hash[:parameters] || tool_hash[:input_schema] + ] + end + # Extracts text content from various message formats. # # Handles string messages, hash messages with :content key, and From 5b55146e4ff318bc1f3f0cce12d1e7e09750810d Mon Sep 17 00:00:00 2001 From: Zane Wolfgang Pickett Date: Wed, 19 Nov 2025 09:57:12 -0800 Subject: [PATCH 07/17] Refactor Tool Choice Clearing --- lib/active_agent/providers/_base_provider.rb | 2 + .../providers/anthropic_provider.rb | 33 ++++++---- .../concerns/tool_choice_clearing.rb | 62 +++++++++++++++++++ .../providers/open_ai/chat_provider.rb | 40 ++++++------ .../providers/open_ai/responses_provider.rb | 40 ++++++------ .../providers/open_router_provider.rb | 30 ++------- 6 files changed, 134 insertions(+), 73 deletions(-) create mode 100644 lib/active_agent/providers/concerns/tool_choice_clearing.rb diff --git a/lib/active_agent/providers/_base_provider.rb b/lib/active_agent/providers/_base_provider.rb index db254f3a..63931692 100644 --- a/lib/active_agent/providers/_base_provider.rb +++ b/lib/active_agent/providers/_base_provider.rb @@ -4,6 +4,7 @@ require_relative "concerns/exception_handler" require_relative "concerns/instrumentation" require_relative "concerns/previewable" +require_relative "concerns/tool_choice_clearing" # @private GEM_LOADERS = { @@ -45,6 +46,7 @@ class BaseProvider include ExceptionHandler include Instrumentation include Previewable + include ToolChoiceClearing class ProvidersError < StandardError; end diff --git a/lib/active_agent/providers/anthropic_provider.rb b/lib/active_agent/providers/anthropic_provider.rb index f243355d..b6eb03c4 100644 --- a/lib/active_agent/providers/anthropic_provider.rb +++ b/lib/active_agent/providers/anthropic_provider.rb @@ -39,22 +39,31 @@ def prepare_prompt_request super end - # @api private - def prepare_prompt_request_tools - return unless request.tool_choice - return unless request.tool_choice.respond_to?(:type) + # Extracts function names from Anthropic's tool_use content blocks. + # + # @return [Array] + def extract_used_function_names + message_stack.pluck(:content).flatten.select { _1[:type] == "tool_use" }.pluck(:name) + end - functions_used = message_stack.pluck(:content).flatten.select { _1[:type] == "tool_use" }.pluck(:name) + # Returns true if tool_choice forces any tool use (type == :any). + # + # @return [Boolean] + def tool_choice_forces_required? + return false unless request.tool_choice.respond_to?(:type) - # tool_choice is always a gem model object (ToolChoiceAny, ToolChoiceTool, ToolChoiceAuto) - tool_choice_type = request.tool_choice.type - tool_choice_name = request.tool_choice.respond_to?(:name) ? request.tool_choice.name : nil + request.tool_choice.type == :any + end - if (tool_choice_type == :any && functions_used.any?) || - (tool_choice_type == :tool && tool_choice_name && functions_used.include?(tool_choice_name)) + # Returns [true, name] if tool_choice forces a specific tool (type == :tool). + # + # @return [Array] + def tool_choice_forces_specific? + return [ false, nil ] unless request.tool_choice.respond_to?(:type) + return [ false, nil ] unless request.tool_choice.type == :tool - request.tool_choice = nil - end + tool_name = request.tool_choice.respond_to?(:name) ? request.tool_choice.name : nil + [ true, tool_name ] end # @api private diff --git a/lib/active_agent/providers/concerns/tool_choice_clearing.rb b/lib/active_agent/providers/concerns/tool_choice_clearing.rb new file mode 100644 index 00000000..27c369f0 --- /dev/null +++ b/lib/active_agent/providers/concerns/tool_choice_clearing.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +module ActiveAgent + module Providers + # Provides unified logic for clearing tool_choice after tool execution. + # + # When a tool_choice is set to "required" or to a specific tool name, + # it forces the model to use that tool. After the tool is executed, + # we need to clear the tool_choice to prevent infinite loops where + # the model keeps calling the same tool repeatedly. + # + # Each provider implements: + # - `extract_used_function_names`: Returns array of tool names that have been called + # - `tool_choice_forces_required?`: Returns true if tool_choice forces any tool use + # - `tool_choice_forces_specific?`: Returns [true, name] if tool_choice forces specific tool + module ToolChoiceClearing + extend ActiveSupport::Concern + + # @api private + def prepare_prompt_request_tools + return unless request.tool_choice + + functions_used = extract_used_function_names + + # Clear if forcing required and any tool was used + if tool_choice_forces_required? && functions_used.any? + request.tool_choice = nil + return + end + + # Clear if forcing specific tool and that tool was used + forces_specific, tool_name = tool_choice_forces_specific? + if forces_specific && tool_name && functions_used.include?(tool_name) + request.tool_choice = nil + end + end + + private + + # Extracts the list of function names that have been called. + # + # @return [Array] function names + def extract_used_function_names + raise NotImplementedError, "#{self.class} must implement #extract_used_function_names" + end + + # Returns true if tool_choice forces any tool to be used (e.g., "required", "any"). + # + # @return [Boolean] + def tool_choice_forces_required? + raise NotImplementedError, "#{self.class} must implement #tool_choice_forces_required?" + end + + # Returns [true, tool_name] if tool_choice forces a specific tool, [false, nil] otherwise. + # + # @return [Array] + def tool_choice_forces_specific? + raise NotImplementedError, "#{self.class} must implement #tool_choice_forces_specific?" + end + end + end +end diff --git a/lib/active_agent/providers/open_ai/chat_provider.rb b/lib/active_agent/providers/open_ai/chat_provider.rb index 5905ad2e..4c1bd656 100644 --- a/lib/active_agent/providers/open_ai/chat_provider.rb +++ b/lib/active_agent/providers/open_ai/chat_provider.rb @@ -12,6 +12,8 @@ module OpenAI # @see Base # @see https://platform.openai.com/docs/api-reference/chat class ChatProvider < Base + include ToolChoiceClearing + # @return [Class] the options class for this provider def self.options_klass Options @@ -37,30 +39,32 @@ def prepare_prompt_request super end - # @api private - def prepare_prompt_request_tools - return unless request.tool_choice - - # Get list of function calls that have been made - # In Chat API, tool calls are in the assistant message's tool_calls array - functions_used = message_stack + # Extracts function names from Chat API tool_calls in assistant messages. + # + # @return [Array] + def extract_used_function_names + message_stack .select { |msg| msg[:role] == "assistant" && msg[:tool_calls] } .flat_map { |msg| msg[:tool_calls] } .map { |tc| tc.dig(:function, :name) } .compact + end - # Check if tool_choice is a hash (specific tool) or string (auto/required) + # Returns true if tool_choice == "required". + # + # @return [Boolean] + def tool_choice_forces_required? + request.tool_choice == "required" + end + + # Returns [true, name] if tool_choice is a hash with nested function name. + # + # @return [Array] + def tool_choice_forces_specific? if request.tool_choice.is_a?(Hash) - # Specific tool choice - clear if that tool was used - tool_choice_name = request.tool_choice.dig(:function, :name) - if tool_choice_name && functions_used.include?(tool_choice_name) - request.tool_choice = nil - end - elsif request.tool_choice == "required" - # Required tool choice - clear if any tool was used - if functions_used.any? - request.tool_choice = nil - end + [ true, request.tool_choice.dig(:function, :name) ] + else + [ false, nil ] end end diff --git a/lib/active_agent/providers/open_ai/responses_provider.rb b/lib/active_agent/providers/open_ai/responses_provider.rb index ad164a22..2734030e 100644 --- a/lib/active_agent/providers/open_ai/responses_provider.rb +++ b/lib/active_agent/providers/open_ai/responses_provider.rb @@ -13,6 +13,8 @@ module OpenAI # @see Base # @see https://platform.openai.com/docs/api-reference/responses class ResponsesProvider < Base + include ToolChoiceClearing + # @return [Class] def self.options_klass Options @@ -33,29 +35,31 @@ def prepare_prompt_request super end - # @api private - def prepare_prompt_request_tools - return unless request.tool_choice - - # Get list of function calls that have been made - # In Responses API, message_stack items are flat - each item has a type field - functions_used = message_stack + # Extracts function names from Responses API function_call items. + # + # @return [Array] + def extract_used_function_names + message_stack .select { |item| item[:type] == "function_call" } .map { |item| item[:name] } .compact + end - # Check if tool_choice is a gem model object or symbol + # Returns true if tool_choice == :required. + # + # @return [Boolean] + def tool_choice_forces_required? + request.tool_choice == :required + end + + # Returns [true, name] if tool_choice is a ToolChoiceFunction model object. + # + # @return [Array] + def tool_choice_forces_specific? if request.tool_choice.is_a?(::OpenAI::Models::Responses::ToolChoiceFunction) - # Specific tool choice - clear if that tool was used - tool_choice_name = request.tool_choice.name - if tool_choice_name && functions_used.include?(tool_choice_name) - request.tool_choice = nil - end - elsif request.tool_choice == :required - # Required tool choice - clear if any tool was used - if functions_used.any? - request.tool_choice = nil - end + [ true, request.tool_choice.name ] + else + [ false, nil ] end end diff --git a/lib/active_agent/providers/open_router_provider.rb b/lib/active_agent/providers/open_router_provider.rb index 1204d6bc..88a01567 100644 --- a/lib/active_agent/providers/open_router_provider.rb +++ b/lib/active_agent/providers/open_router_provider.rb @@ -40,31 +40,11 @@ def prepare_prompt_request super end - # @api private - def prepare_prompt_request_tools - return unless request.tool_choice - - # Get list of function calls that have been made - # In Chat API, tool calls are in the assistant message's tool_calls array - functions_used = message_stack - .select { |msg| msg[:role] == "assistant" && msg[:tool_calls] } - .flat_map { |msg| msg[:tool_calls] } - .map { |tc| tc.dig(:function, :name) } - .compact - - # Check if tool_choice is a hash (specific tool) or string (auto/any) - if request.tool_choice.is_a?(Hash) - # Specific tool choice - clear if that tool was used - tool_choice_name = request.tool_choice.dig(:function, :name) - if tool_choice_name && functions_used.include?(tool_choice_name) - request.tool_choice = nil - end - elsif request.tool_choice == "any" - # OpenRouter uses "any" for required - clear if any tool was used - if functions_used.any? - request.tool_choice = nil - end - end + # Returns true if tool_choice == "any" (OpenRouter's equivalent of "required"). + # + # @return [Boolean] + def tool_choice_forces_required? + request.tool_choice == "any" end # Merges streaming delta into the message with role cleanup. From 6f1486cabc564c2ca62aec2455ddf9846dec778e Mon Sep 17 00:00:00 2001 From: Zane Wolfgang Pickett Date: Wed, 19 Nov 2025 10:21:05 -0800 Subject: [PATCH 08/17] Update Tools Docs for Common Format --- docs/actions/tools.md | 77 ++- test/docs/actions/tools_examples_test.rb | 152 ++++-- test/docs/actions_examples_test.rb | 1 - .../actions/tools/cross_provider_usage.yml | 448 ++++++++++++++++++ 4 files changed, 631 insertions(+), 47 deletions(-) create mode 100644 test/fixtures/vcr_cassettes/docs/actions/tools/cross_provider_usage.yml diff --git a/docs/actions/tools.md b/docs/actions/tools.md index e649d666..0a040282 100644 --- a/docs/actions/tools.md +++ b/docs/actions/tools.md @@ -25,13 +25,13 @@ The LLM calls `get_weather` automatically when it needs weather data, and uses t | **Ollama** | 🟩 | ❌ | ❌ | Model-dependent capabilities | | **Mock** | 🟦 | ❌ | ❌ | Accepted but not enforced | -## Functions (Universal Support) +## Functions -Functions are the core tool capability supported by all providers. Define methods in your agent that the LLM can call with appropriate parameters. +Functions are callable methods in your agent that LLMs can trigger with appropriate parameters. All providers support the **common format** described above. ### Basic Function Registration -Register functions by passing tool definitions to the `tools` parameter: +Using the common format, register functions by passing tool definitions to the `tools` parameter: ::: code-group <<< @/../test/docs/actions/tools_examples_test.rb#anthropic_basic_function {ruby:line-numbers} [Anthropic] @@ -42,24 +42,83 @@ Register functions by passing tool definitions to the `tools` parameter: When the LLM decides to call a tool, ActiveAgent routes the call to your agent method and returns the result automatically. +## Common Tools Format (Recommended) + +ActiveAgent supports a **universal common format** for tool definitions that works seamlessly across all providers. This format eliminates the need to learn provider-specific syntax and makes your code portable. + +### Format Specification + +```ruby +{ + name: "function_name", # Required: function name to call + description: "What it does", # Required: clear description for LLM + parameters: { # Required: JSON Schema for parameters + type: "object", + properties: { + param_name: { + type: "string", + description: "Parameter description" + } + }, + required: ["param_name"] + } +} +``` + +### Cross-Provider Example + +The same tool definition works everywhere: + +<<< @/../test/docs/actions/tools_examples_test.rb#cross_provider_module {ruby:line-numbers} + +::: code-group +<<< @/../test/docs/actions/tools_examples_test.rb#cross_provider_anthropic {ruby:line-numbers} [Anthropic] +<<< @/../test/docs/actions/tools_examples_test.rb#cross_provider_ollama{ruby:line-numbers} [Ollama] +<<< @/../test/docs/actions/tools_examples_test.rb#cross_provider_openai{ruby:line-numbers} [OpenAI] +<<< @/../test/docs/actions/tools_examples_test.rb#cross_provider_openrouter {ruby:line-numbers} [OpenRouter] +::: + +### Alternative: `input_schema` Key + +You can also use `input_schema` instead of `parameters` - both work identically: + +```ruby +{ + name: "get_weather", + description: "Get current weather", + input_schema: { # Alternative to 'parameters' + type: "object", + properties: { ... } + } +} +``` + +ActiveAgent automatically converts between common format and each provider's native format behind the scenes. + ### Tool Choice Control -Control which tools the LLM can use: +Control when and which tools the LLM uses with the `tool_choice` parameter: ```ruby -# Let the model decide (default) +# Auto (default) - Let the model decide whether to use tools prompt(message: "...", tools: tools, tool_choice: "auto") -# Force the model to use a tool +# Required - Force the model to use at least one tool prompt(message: "...", tools: tools, tool_choice: "required") -# Prevent tool usage +# None - Prevent tool usage entirely prompt(message: "...", tools: tools, tool_choice: "none") -# Force a specific tool (provider-dependent) -prompt(message: "...", tools: tools, tool_choice: { type: "function", name: "get_weather" }) +# Specific tool - Force a particular tool (common format) +prompt(message: "...", tools: tools, tool_choice: { name: "get_weather" }) ``` +ActiveAgent automatically maps these common values to provider-specific formats: +- **OpenAI**: `"auto"`, `"required"`, `"none"`, or `{type: "function", function: {name: "..."}}` +- **Anthropic**: `{type: :auto}`, `{type: :any}`, `{type: :tool, name: "..."}` +- **OpenRouter**: `"auto"`, `"any"` (equivalent to "required") +- **Ollama**: Model-dependent tool choice support + ## Server-Side Tools (Provider-Specific) Some providers offer built-in tools that run on their servers, providing capabilities like web search and code execution without custom implementation. diff --git a/test/docs/actions/tools_examples_test.rb b/test/docs/actions/tools_examples_test.rb index 9f68149f..396cf27e 100644 --- a/test/docs/actions/tools_examples_test.rb +++ b/test/docs/actions/tools_examples_test.rb @@ -12,7 +12,6 @@ def weather_update prompt( input: "What's the weather in Boston?", tools: [ { - type: "function", name: "get_weather", description: "Get current weather for a location", parameters: { @@ -55,7 +54,6 @@ def weather_update prompt( input: "What's the weather in Boston?", tools: [ { - type: "function", name: "get_current_weather", description: "Get the current weather in a given location", parameters: { @@ -105,7 +103,7 @@ def weather_update tools: [ { name: "get_weather", description: "Get the current weather in a given location", - input_schema: { + parameters: { type: "object", properties: { location: { @@ -145,24 +143,21 @@ def weather_update prompt( message: "What's the weather in Boston?", tools: [ { - type: "function", - function: { - name: "get_current_weather", - description: "Get the current weather in a given location", - parameters: { - type: "object", - properties: { - location: { - type: "string", - description: "The city and state, e.g. San Francisco, CA" - }, - unit: { - type: "string", - enum: [ "celsius", "fahrenheit" ] - } + name: "get_current_weather", + description: "Get the current weather in a given location", + parameters: { + type: "object", + properties: { + location: { + type: "string", + description: "The city and state, e.g. San Francisco, CA" }, - required: [ "location" ] - } + unit: { + type: "string", + enum: [ "celsius", "fahrenheit" ] + } + }, + required: [ "location" ] } } ] ) @@ -194,24 +189,21 @@ def weather_update prompt( message: "What's the weather in Boston?", tools: [ { - type: "function", - function: { - name: "get_current_weather", - description: "Get the current weather in a given location", - parameters: { - type: "object", - properties: { - location: { - type: "string", - description: "The city and state, e.g. San Francisco, CA" - }, - unit: { - type: "string", - enum: [ "celsius", "fahrenheit" ] - } + name: "get_current_weather", + description: "Get the current weather in a given location", + parameters: { + type: "object", + properties: { + location: { + type: "string", + description: "The city and state, e.g. San Francisco, CA" }, - required: [ "location" ] - } + unit: { + type: "string", + enum: [ "celsius", "fahrenheit" ] + } + }, + required: [ "location" ] } } ] ) @@ -233,6 +225,92 @@ def get_current_weather(location:, unit: "fahrenheit") end end end + + class CrossProviderExample < ActiveSupport::TestCase + test "cross provider usage" do + VCR.use_cassette("docs/actions/tools/cross_provider_usage") do + # region cross_provider_module + # Define once, use with any provider + module WeatherTool + extend ActiveSupport::Concern + + WEATHER_TOOL = { + name: "get_weather", + description: "Get current weather for a location", + parameters: { + type: "object", + properties: { + location: { type: "string", description: "City and state" }, + unit: { type: "string", enum: [ "celsius", "fahrenheit" ] } + }, + required: [ "location" ] + } + } + + def get_current_weather(location:, unit: "fahrenheit") + { location: location, unit: unit, temperature: "22" } + end + end + # endregion cross_provider_module + + # region cross_provider_openai + class OpenAIAgent < ApplicationAgent + include WeatherTool + generate_with :openai, model: "gpt-4o" + + def check_weather + prompt(input: "What's the weather?", tools: [ WEATHER_TOOL ]) + end + end + # endregion cross_provider_openai + + # region cross_provider_anthropic + class AnthropicAgent < ApplicationAgent + include WeatherTool + generate_with :anthropic, model: "claude-sonnet-4-20250514" + + def check_weather + prompt(message: "What's the weather?", tools: [ WEATHER_TOOL ]) + end + end + # endregion cross_provider_anthropic + + # region cross_provider_ollama + class OllamaAgent < ApplicationAgent + include WeatherTool + generate_with :ollama, model: "qwen3:latest" + + def check_weather + prompt(message: "What's the weather?", tools: [ WEATHER_TOOL ]) + end + end + # endregion cross_provider_ollama + + # region cross_provider_openrouter + class OpenRouterAgent < ApplicationAgent + include WeatherTool + generate_with :openrouter, model: "google/gemini-2.0-flash-001" + + def check_weather + prompt(message: "What's the weather?", tools: [ WEATHER_TOOL ]) + end + end + # endregion cross_provider_openrouter + + response = OpenAIAgent.check_weather.generate_now + assert response.message.content.present? + + response = AnthropicAgent.check_weather.generate_now + assert response.message.content.present? + + response = OllamaAgent.check_weather.generate_now + assert response.message.content.present? + + response = OpenRouterAgent.check_weather.generate_now + assert response.message.content.present? + end + end + end end end end diff --git a/test/docs/actions_examples_test.rb b/test/docs/actions_examples_test.rb index 45de6a82..f9d0b590 100644 --- a/test/docs/actions_examples_test.rb +++ b/test/docs/actions_examples_test.rb @@ -55,7 +55,6 @@ def weather_update prompt( input: "What's the weather in Boston?", tools: [ { - type: "function", name: "get_current_weather", description: "Get the current weather in a given location", parameters: { diff --git a/test/fixtures/vcr_cassettes/docs/actions/tools/cross_provider_usage.yml b/test/fixtures/vcr_cassettes/docs/actions/tools/cross_provider_usage.yml new file mode 100644 index 00000000..9f9e4250 --- /dev/null +++ b/test/fixtures/vcr_cassettes/docs/actions/tools/cross_provider_usage.yml @@ -0,0 +1,448 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.openai.com/v1/responses + body: + encoding: UTF-8 + string: '{"model":"gpt-4o","input":"What''s the weather?","tools":[{"type":"function","name":"get_weather","description":"Get + current weather for a location","parameters":{"type":"object","properties":{"location":{"type":"string","description":"City + and state"},"unit":{"type":"string","enum":["celsius","fahrenheit"]}},"required":["location"]}}]}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json + User-Agent: + - OpenAI::Client/Ruby 0.35.1 + Host: + - api.openai.com + X-Stainless-Arch: + - arm64 + X-Stainless-Lang: + - ruby + X-Stainless-Os: + - MacOS + X-Stainless-Package-Version: + - 0.35.1 + X-Stainless-Runtime: + - ruby + X-Stainless-Runtime-Version: + - 3.4.7 + Content-Type: + - application/json + Openai-Organization: + - ORGANIZATION_ID + Openai-Project: + - PROJECT_ID + Authorization: + - Bearer ACCESS_TOKEN + X-Stainless-Retry-Count: + - '0' + X-Stainless-Timeout: + - '600.0' + Content-Length: + - '337' + response: + status: + code: 200 + message: OK + headers: + Date: + - Wed, 19 Nov 2025 18:18:33 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + X-Ratelimit-Limit-Requests: + - '10000' + X-Ratelimit-Limit-Tokens: + - '30000000' + X-Ratelimit-Remaining-Requests: + - '9999' + X-Ratelimit-Remaining-Tokens: + - '29999725' + X-Ratelimit-Reset-Requests: + - 6ms + X-Ratelimit-Reset-Tokens: + - 0s + Openai-Version: + - '2020-10-01' + Openai-Organization: + - ORGANIZATION_ID + Openai-Project: + - PROJECT_ID + X-Request-Id: + - req_8513283bef2e4a1faf08f9d91b8debdb + Openai-Processing-Ms: + - '1782' + X-Envoy-Upstream-Service-Time: + - '1786' + Cf-Cache-Status: + - DYNAMIC + Set-Cookie: + - __cf_bm=k_pHGl_NaO7cz7cXFUH461e6R3jh2QlExmfoMdyRMmY-1763576313-1.0.1.1-xomS.7WXMfUlaxu8GUfrYZdZg0W8_vsG_PHcWjzvSAKpDctb1ztMqMDCNpoCmhmLYmbFcAYL8Q5S2zkKRSCMF6HYj0I4G_Wyf6uQAHBKrmw; + path=/; expires=Wed, 19-Nov-25 18:48:33 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=AFN0qelmGZnujGNMi3.dkE6SkuXAnw1oMNw8lM_.AAY-1763576313407-0.0.1.1-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + X-Content-Type-Options: + - nosniff + Server: + - cloudflare + Cf-Ray: + - 9a11b5e6d9ba67fb-SJC + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: |- + { + "id": "resp_0bc6fad39e50a44b00691e09f79f44819b8a01809fe1c1e1e1", + "object": "response", + "created_at": 1763576311, + "status": "completed", + "background": false, + "billing": { + "payer": "developer" + }, + "error": null, + "incomplete_details": null, + "instructions": null, + "max_output_tokens": null, + "max_tool_calls": null, + "model": "gpt-4o-2024-08-06", + "output": [ + { + "id": "msg_0bc6fad39e50a44b00691e09f8d6bc819bb5b189268456efea", + "type": "message", + "status": "completed", + "content": [ + { + "type": "output_text", + "annotations": [], + "logprobs": [], + "text": "Could you please specify the location for which you'd like to know the weather?" + } + ], + "role": "assistant" + } + ], + "parallel_tool_calls": true, + "previous_response_id": null, + "prompt_cache_key": null, + "prompt_cache_retention": null, + "reasoning": { + "effort": null, + "summary": null + }, + "safety_identifier": null, + "service_tier": "default", + "store": true, + "temperature": 1.0, + "text": { + "format": { + "type": "text" + }, + "verbosity": "medium" + }, + "tool_choice": "auto", + "tools": [ + { + "type": "function", + "description": "Get current weather for a location", + "name": "get_weather", + "parameters": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "City and state" + }, + "unit": { + "type": "string", + "enum": [ + "celsius", + "fahrenheit" + ] + } + }, + "required": [ + "location", + "unit" + ], + "additionalProperties": false + }, + "strict": true + } + ], + "top_logprobs": 0, + "top_p": 1.0, + "truncation": "disabled", + "usage": { + "input_tokens": 58, + "input_tokens_details": { + "cached_tokens": 0 + }, + "output_tokens": 17, + "output_tokens_details": { + "reasoning_tokens": 0 + }, + "total_tokens": 75 + }, + "user": null, + "metadata": {} + } + recorded_at: Wed, 19 Nov 2025 18:18:33 GMT +- request: + method: post + uri: https://api.anthropic.com/v1/messages + body: + encoding: UTF-8 + string: '{"model":"claude-sonnet-4-20250514","messages":[{"content":"What''s + the weather?","role":"user"}],"tools":[{"input_schema":{"type":"object","properties":{"location":{"type":"string","description":"City + and state"},"unit":{"type":"string","enum":["celsius","fahrenheit"]}},"required":["location"]},"name":"get_weather","description":"Get + current weather for a location"}],"max_tokens":4096}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json + User-Agent: + - Anthropic::Client/Ruby 1.14.0 + Host: + - api.anthropic.com + X-Stainless-Arch: + - arm64 + X-Stainless-Lang: + - ruby + X-Stainless-Os: + - MacOS + X-Stainless-Package-Version: + - 1.14.0 + X-Stainless-Runtime: + - ruby + X-Stainless-Runtime-Version: + - 3.4.7 + Content-Type: + - application/json + Anthropic-Version: + - '2023-06-01' + X-Api-Key: + - ACCESS_TOKEN + X-Stainless-Retry-Count: + - '0' + X-Stainless-Timeout: + - '600.0' + Content-Length: + - '388' + response: + status: + code: 200 + message: OK + headers: + Date: + - Wed, 19 Nov 2025 18:18:36 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Anthropic-Ratelimit-Input-Tokens-Limit: + - '2000000' + Anthropic-Ratelimit-Input-Tokens-Remaining: + - '2000000' + Anthropic-Ratelimit-Input-Tokens-Reset: + - '2025-11-19T18:18:35Z' + Anthropic-Ratelimit-Output-Tokens-Limit: + - '400000' + Anthropic-Ratelimit-Output-Tokens-Remaining: + - '400000' + Anthropic-Ratelimit-Output-Tokens-Reset: + - '2025-11-19T18:18:36Z' + Anthropic-Ratelimit-Requests-Limit: + - '4000' + Anthropic-Ratelimit-Requests-Remaining: + - '3999' + Anthropic-Ratelimit-Requests-Reset: + - '2025-11-19T18:18:33Z' + Retry-After: + - '25' + Anthropic-Ratelimit-Tokens-Limit: + - '2400000' + Anthropic-Ratelimit-Tokens-Remaining: + - '2400000' + Anthropic-Ratelimit-Tokens-Reset: + - '2025-11-19T18:18:35Z' + Request-Id: + - req_011CVHi3YRg6Ci88kiLM46kE + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Anthropic-Organization-Id: + - 2557c2f2-bcfa-4054-9fa8-ae13b3b47d6b + X-Envoy-Upstream-Service-Time: + - '2614' + Cf-Cache-Status: + - DYNAMIC + X-Robots-Tag: + - none + Server: + - cloudflare + Cf-Ray: + - 9a11b5f74c88bffc-SJC + body: + encoding: ASCII-8BIT + string: '{"model":"claude-sonnet-4-20250514","id":"msg_01SX7S6wKVyFVdm4hPJ4Mbjc","type":"message","role":"assistant","content":[{"type":"text","text":"I''d + be happy to help you get the weather information! However, I need to know + which location you''d like the weather for. Could you please tell me the city + and state (or city and country) you''re interested in?"}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":407,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":50,"service_tier":"standard"}}' + recorded_at: Wed, 19 Nov 2025 18:18:36 GMT +- request: + method: post + uri: http://127.0.0.1:11434/v1/chat/completions + body: + encoding: UTF-8 + string: '{"model":"qwen3:latest","messages":[{"role":"user","content":"What''s + the weather?"}],"tools":[{"type":"function","function":{"name":"get_weather","description":"Get + current weather for a location","parameters":{"type":"object","properties":{"location":{"type":"string","description":"City + and state"},"unit":{"type":"string","enum":["celsius","fahrenheit"]}},"required":["location"]}}}]}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json + User-Agent: + - OpenAI::Client/Ruby 0.35.1 + Host: + - 127.0.0.1:11434 + X-Stainless-Arch: + - arm64 + X-Stainless-Lang: + - ruby + X-Stainless-Os: + - MacOS + X-Stainless-Package-Version: + - 0.35.1 + X-Stainless-Runtime: + - ruby + X-Stainless-Runtime-Version: + - 3.4.7 + Content-Type: + - application/json + Authorization: + - Bearer ollama + X-Stainless-Retry-Count: + - '0' + X-Stainless-Timeout: + - '600.0' + Content-Length: + - '387' + response: + status: + code: 200 + message: OK + headers: + Content-Type: + - application/json + Date: + - Wed, 19 Nov 2025 18:18:45 GMT + Content-Length: + - '937' + body: + encoding: UTF-8 + string: '{"id":"chatcmpl-178","object":"chat.completion","created":1763576325,"model":"qwen3:latest","system_fingerprint":"fp_ollama","choices":[{"index":0,"message":{"role":"assistant","content":"I + need more details to check the weather. Could you please tell me the location + (city and state) and whether you want the temperature in Celsius or Fahrenheit?","reasoning":"Okay, + the user asked, \"What''s the weather?\" I need to figure out how to respond. + Let me check the tools provided. There''s a get_weather function, but it requires + parameters like location and unit. The user didn''t specify a location or + unit. I should ask them for more details. Let me confirm if they want the + weather for a specific city and whether they prefer Celsius or Fahrenheit. + That way, I can call the function properly once I have all the necessary information.\n"},"finish_reason":"stop"}],"usage":{"prompt_tokens":161,"completion_tokens":136,"total_tokens":297}} + + ' + recorded_at: Wed, 19 Nov 2025 18:18:45 GMT +- request: + method: post + uri: https://openrouter.ai/api/v1/chat/completions + body: + encoding: UTF-8 + string: '{"model":"google/gemini-2.0-flash-001","messages":[{"role":"user","content":"What''s + the weather?"}],"tools":[{"type":"function","function":{"name":"get_weather","description":"Get + current weather for a location","parameters":{"type":"object","properties":{"location":{"type":"string","description":"City + and state"},"unit":{"type":"string","enum":["celsius","fahrenheit"]}},"required":["location"]}}}]}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json + User-Agent: + - OpenAI::Client/Ruby 0.35.1 + Host: + - openrouter.ai + X-Stainless-Arch: + - arm64 + X-Stainless-Lang: + - ruby + X-Stainless-Os: + - MacOS + X-Stainless-Package-Version: + - 0.35.1 + X-Stainless-Runtime: + - ruby + X-Stainless-Runtime-Version: + - 3.4.7 + Content-Type: + - application/json + Authorization: + - Bearer ACCESS_TOKEN + Http-Referer: + - https://example.com + X-Title: + - Dummy + X-Stainless-Retry-Count: + - '0' + X-Stainless-Timeout: + - '600.0' + Content-Length: + - '402' + response: + status: + code: 200 + message: OK + headers: + Date: + - Wed, 19 Nov 2025 18:18:46 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Access-Control-Allow-Origin: + - "*" + Vary: + - Accept-Encoding + Permissions-Policy: + - payment=(self "https://checkout.stripe.com" "https://connect-js.stripe.com" + "https://js.stripe.com" "https://*.js.stripe.com" "https://hooks.stripe.com") + Referrer-Policy: + - no-referrer, strict-origin-when-cross-origin + X-Content-Type-Options: + - nosniff + Server: + - cloudflare + Cf-Ray: + - 9a11b644cedeee17-SJC + body: + encoding: ASCII-8BIT + string: "\n \n{\"id\":\"gen-1763576326-GG7DVgNI8Zxze0XPOD7X\",\"provider\":\"Google\",\"model\":\"google/gemini-2.0-flash-001\",\"object\":\"chat.completion\",\"created\":1763576326,\"choices\":[{\"logprobs\":null,\"finish_reason\":\"stop\",\"native_finish_reason\":\"STOP\",\"index\":0,\"message\":{\"role\":\"assistant\",\"content\":\"To + provide you with the weather, I need to know your location. Could you please + tell me which city and state you're in?\\n\",\"refusal\":null,\"reasoning\":null}}],\"usage\":{\"prompt_tokens\":28,\"completion_tokens\":29,\"total_tokens\":57,\"prompt_tokens_details\":{\"cached_tokens\":0},\"completion_tokens_details\":{\"reasoning_tokens\":0,\"image_tokens\":0}}}" + recorded_at: Wed, 19 Nov 2025 18:18:46 GMT +recorded_with: VCR 6.3.1 From b74d7f77896916d7e0c8a80f613ad57b3a9b61bb Mon Sep 17 00:00:00 2001 From: Zane Wolfgang Pickett Date: Wed, 19 Nov 2025 10:39:36 -0800 Subject: [PATCH 09/17] Update CHANGELOG --- CHANGELOG.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d112737..71d7740a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -111,6 +111,29 @@ Template paths: ### Added +**Universal Tools Format** +```ruby +# Single format works across all providers (Anthropic, OpenAI, OpenRouter, Ollama, Mock) +tools: [{ + name: "get_weather", + description: "Get current weather", + parameters: { + type: "object", + properties: { + location: { type: "string", description: "City and state" } + }, + required: ["location"] + } +}] + +# Tool choice normalization +tool_choice: "auto" # Let model decide +tool_choice: "required" # Force tool use +tool_choice: { name: "get_weather" } # Force specific tool +``` + +Automatic conversion to provider-specific formats. Old formats still work (backward compatible). + **Mock Provider for Testing** ```ruby class MyAgent < ActiveAgent::Base @@ -196,6 +219,7 @@ response.usage.service_tier # Anthropic - Retry logic moved to provider SDKs (automatic exponential backoff) - Migrated to official SDKs: `openai` gem and `anthropic` gem - Type-safe options with per-provider definitions +- Shared `ToolChoiceClearing` concern eliminates duplication across providers **Configuration** - Options configurable at class level, instance level, or per-call From 9887f25607e0960f7bc38fa4237f57d2d48d4cb5 Mon Sep 17 00:00:00 2001 From: Zane Wolfgang Pickett Date: Wed, 19 Nov 2025 12:36:18 -0800 Subject: [PATCH 10/17] Add Anthropic [Beta] MCP Support --- .../providers/anthropic/options.rb | 10 +- .../providers/anthropic/request.rb | 31 ++- .../providers/anthropic/transforms.rb | 43 ++- .../providers/anthropic_provider.rb | 9 +- .../test_agent_common_format_mixed_auth.yml | 141 ++++++++++ ...t_agent_common_format_multiple_servers.yml | 142 ++++++++++ ...test_agent_common_format_single_server.yml | 110 ++++++++ ..._common_format_single_server_with_auth.yml | 140 ++++++++++ .../test_agent_common_format_sse_server.yml | 109 ++++++++ .../anthropic/common_format/mcp_test.rb | 256 ++++++++++++++++++ test/providers/anthropic/transforms_test.rb | 120 +++++++- test/test_helper.rb | 11 +- 12 files changed, 1102 insertions(+), 20 deletions(-) create mode 100644 test/fixtures/vcr_cassettes/integration/anthropic/common_format/mcp_test/test_agent_common_format_mixed_auth.yml create mode 100644 test/fixtures/vcr_cassettes/integration/anthropic/common_format/mcp_test/test_agent_common_format_multiple_servers.yml create mode 100644 test/fixtures/vcr_cassettes/integration/anthropic/common_format/mcp_test/test_agent_common_format_single_server.yml create mode 100644 test/fixtures/vcr_cassettes/integration/anthropic/common_format/mcp_test/test_agent_common_format_single_server_with_auth.yml create mode 100644 test/fixtures/vcr_cassettes/integration/anthropic/common_format/mcp_test/test_agent_common_format_sse_server.yml create mode 100644 test/integration/anthropic/common_format/mcp_test.rb diff --git a/lib/active_agent/providers/anthropic/options.rb b/lib/active_agent/providers/anthropic/options.rb index 898d3027..2160e412 100644 --- a/lib/active_agent/providers/anthropic/options.rb +++ b/lib/active_agent/providers/anthropic/options.rb @@ -28,15 +28,13 @@ def initialize(kwargs = {}) end def serialize - super.except(:anthropic_beta).tap do |hash| - hash[:extra_headers] = extra_headers unless extra_headers.blank? - end + super.except(:anthropic_beta) end + # Anthropic gem handles beta headers differently via client.beta + # rather than via extra_headers in request_options def extra_headers - deep_compact( - "anthropic-beta" => anthropic_beta.presence, - ) + {} end private diff --git a/lib/active_agent/providers/anthropic/request.rb b/lib/active_agent/providers/anthropic/request.rb index 08419c23..ccc33b91 100644 --- a/lib/active_agent/providers/anthropic/request.rb +++ b/lib/active_agent/providers/anthropic/request.rb @@ -67,11 +67,13 @@ class Request < SimpleDelegator # @option params [Array] :messages required # @option params [Integer] :max_tokens (4096) # @option params [Hash] :response_format custom field for JSON mode simulation + # @option params [String] :anthropic_beta beta version for features like MCP # @raise [ArgumentError] when gem model validation fails def initialize(**params) # Step 1: Extract custom fields that gem doesn't support @response_format = params.delete(:response_format) @stream = params.delete(:stream) + anthropic_beta = params.delete(:anthropic_beta) # Step 2: Map common format 'instructions' to Anthropic's 'system' if params.key?(:instructions) @@ -84,10 +86,24 @@ def initialize(**params) # Step 4: Transform params for gem compatibility transformed = Transforms.normalize_params(params) - # Step 5: Create gem model - this validates all parameters! - gem_model = ::Anthropic::Models::MessageCreateParams.new(**transformed) + # Step 5: Determine if we need beta params (for MCP or other beta features) + use_beta = anthropic_beta.present? || transformed[:mcp_servers]&.any? - # Step 6: Delegate all method calls to gem model + # Step 6: Add betas parameter if using beta API + if use_beta + # Default to MCP beta version if not specified + beta_version = anthropic_beta || "mcp-client-2025-04-04" + transformed[:betas] = [ beta_version ] + end + + # Step 7: Create gem model - use Beta version if needed + gem_model = if use_beta + ::Anthropic::Models::Beta::MessageCreateParams.new(**transformed) + else + ::Anthropic::Models::MessageCreateParams.new(**transformed) + end + + # Step 8: Delegate all method calls to gem model super(gem_model) rescue ArgumentError => e # Re-raise with more context @@ -134,6 +150,15 @@ def instructions=(value) self.system = value end + # Accessor for MCP servers. + # + # Safely returns MCP servers array, defaulting to empty array if not set. + # + # @return [Array] + def mcp_servers + __getobj__.instance_variable_get(:@data)[:mcp_servers] || [] + end + # Removes the last message from the messages array. # # Used for JSON format simulation to remove the lead-in assistant message. diff --git a/lib/active_agent/providers/anthropic/transforms.rb b/lib/active_agent/providers/anthropic/transforms.rb index e1c6a736..8214f845 100644 --- a/lib/active_agent/providers/anthropic/transforms.rb +++ b/lib/active_agent/providers/anthropic/transforms.rb @@ -30,6 +30,7 @@ def normalize_params(params) params[:system] = normalize_system(params[:system]) if params[:system] params[:tools] = normalize_tools(params[:tools]) if params[:tools] params[:tool_choice] = normalize_tool_choice(params[:tool_choice]) if params[:tool_choice] + params[:mcp_servers] = normalize_mcp_servers(params[:mcp_servers]) if params[:mcp_servers] params end @@ -58,6 +59,44 @@ def normalize_tools(tools) end end + # Normalizes MCP servers from common format to Anthropic format. + # + # Common format: + # {name: "stripe", url: "https://...", authorization: "token"} + # Anthropic format: + # {type: "url", name: "stripe", url: "https://...", authorization_token: "token"} + # + # @param mcp_servers [Array] + # @return [Array] + def normalize_mcp_servers(mcp_servers) + return mcp_servers unless mcp_servers.is_a?(Array) + + mcp_servers.map do |server| + server_hash = server.is_a?(Hash) ? server.deep_symbolize_keys : server + + # If already in Anthropic format (has type: "url" and authorization_token), return as-is + if server_hash[:type] == "url" && !server_hash[:authorization] + next server_hash + end + + # Convert common format to Anthropic format + result = { + type: "url", + name: server_hash[:name], + url: server_hash[:url] + } + + # Map 'authorization' to 'authorization_token' + if server_hash[:authorization] + result[:authorization_token] = server_hash[:authorization] + elsif server_hash[:authorization_token] + result[:authorization_token] = server_hash[:authorization_token] + end + + result.compact + end + end + # Normalizes tool_choice from common format to Anthropic gem model objects. # # The Anthropic gem expects tool_choice to be a model object (ToolChoiceAuto, @@ -398,9 +437,9 @@ def cleanup_serialized_request(hash, defaults, gem_object = nil) # Apply content compression for API efficiency compress_content(hash) - # Remove provider-internal fields that should not be in API request - hash.delete(:mcp_servers) # Provider-level config, not API param + # Remove provider-internal fields and empty arrays hash.delete(:stop_sequences) if hash[:stop_sequences] == [] + hash.delete(:mcp_servers) if hash[:mcp_servers] == [] hash.delete(:tool_choice) if hash[:tool_choice].nil? # Don't send null tool_choice # Remove default values (except max_tokens which is required by API) diff --git a/lib/active_agent/providers/anthropic_provider.rb b/lib/active_agent/providers/anthropic_provider.rb index b6eb03c4..1bdb0009 100644 --- a/lib/active_agent/providers/anthropic_provider.rb +++ b/lib/active_agent/providers/anthropic_provider.rb @@ -77,9 +77,14 @@ def prepare_prompt_request_response_format end # @see BaseProvider#api_prompt_executer - # @return [Anthropic::Messages] + # @return [Anthropic::Messages, Anthropic::Resources::Beta::Messages] def api_prompt_executer - client.messages + # Use beta API when anthropic_beta option is set or when using MCP servers + if options.anthropic_beta.present? || request.mcp_servers&.any? + client.beta.messages + else + client.messages + end end # @see BaseProvider#api_response_normalize diff --git a/test/fixtures/vcr_cassettes/integration/anthropic/common_format/mcp_test/test_agent_common_format_mixed_auth.yml b/test/fixtures/vcr_cassettes/integration/anthropic/common_format/mcp_test/test_agent_common_format_mixed_auth.yml new file mode 100644 index 00000000..6738bbd7 --- /dev/null +++ b/test/fixtures/vcr_cassettes/integration/anthropic/common_format/mcp_test/test_agent_common_format_mixed_auth.yml @@ -0,0 +1,141 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.anthropic.com/v1/messages?beta=true + body: + encoding: UTF-8 + string: '{"model":"claude-haiku-4-5","max_tokens":1024,"messages":[{"content":"What + tools do you have available?","role":"user"}],"mcp_servers":[{"name":"cloudflare-demo","type":"url","url":"https://demo-day.mcp.cloudflare.com/sse"},{"name":"github-copilot","type":"url","url":"https://api.githubcopilot.com/mcp/","authorization_token":"GITHUB_MCP_TOKEN"}]}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json + User-Agent: + - Anthropic::Client/Ruby 1.14.0 + Host: + - api.anthropic.com + X-Stainless-Arch: + - arm64 + X-Stainless-Lang: + - ruby + X-Stainless-Os: + - MacOS + X-Stainless-Package-Version: + - 1.14.0 + X-Stainless-Runtime: + - ruby + X-Stainless-Runtime-Version: + - 3.4.7 + Content-Type: + - application/json + Anthropic-Version: + - '2023-06-01' + X-Api-Key: + - ACCESS_TOKEN + Anthropic-Beta: + - mcp-client-2025-04-04 + X-Stainless-Retry-Count: + - '0' + X-Stainless-Timeout: + - '600.0' + Content-Length: + - '425' + response: + status: + code: 200 + message: OK + headers: + Date: + - Wed, 19 Nov 2025 20:30:50 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Anthropic-Ratelimit-Input-Tokens-Limit: + - '4000000' + Anthropic-Ratelimit-Input-Tokens-Remaining: + - '3996000' + Anthropic-Ratelimit-Input-Tokens-Reset: + - '2025-11-19T20:30:43Z' + Anthropic-Ratelimit-Output-Tokens-Limit: + - '800000' + Anthropic-Ratelimit-Output-Tokens-Remaining: + - '799000' + Anthropic-Ratelimit-Output-Tokens-Reset: + - '2025-11-19T20:30:50Z' + Anthropic-Ratelimit-Requests-Limit: + - '4000' + Anthropic-Ratelimit-Requests-Remaining: + - '3999' + Anthropic-Ratelimit-Requests-Reset: + - '2025-11-19T20:30:42Z' + Retry-After: + - '18' + Anthropic-Ratelimit-Tokens-Limit: + - '4800000' + Anthropic-Ratelimit-Tokens-Remaining: + - '4795000' + Anthropic-Ratelimit-Tokens-Reset: + - '2025-11-19T20:30:43Z' + Request-Id: + - req_011CVHt7uDMUSpVrehmUC1kz + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Anthropic-Organization-Id: + - 2557c2f2-bcfa-4054-9fa8-ae13b3b47d6b + X-Envoy-Upstream-Service-Time: + - '9375' + Cf-Cache-Status: + - DYNAMIC + X-Robots-Tag: + - none + Server: + - cloudflare + Cf-Ray: + - 9a1277811db7aab7-SJC + body: + encoding: ASCII-8BIT + string: '{"model":"claude-haiku-4-5-20251001","id":"msg_01R71HsqqyVcvwkVaHYFyu9o","type":"message","role":"assistant","content":[{"type":"text","text":"I + have a comprehensive set of tools available for working with GitHub and Cloudflare. + Here''s what I can do:\n\n## GitHub Tools\n\n### Repository Management\n- + **Create repositories** - Create new repos in your account or an organization\n- + **Fork repositories** - Fork existing projects\n- **List branches** - View + available branches in a repo\n- **List tags** - View git tags in a repository\n- + **List releases** - View releases in a repository\n\n### File Operations\n- + **Get file contents** - Retrieve files or directory contents\n- **Create or + update files** - Create new files or update existing ones in a repo\n- **Delete + files** - Remove files from a repository\n- **Push files** - Push multiple + files in a single commit\n\n### Branch Management\n- **Create branches** - + Create new branches from a source branch\n\n### Commits\n- **Get commit details** + - View specific commit information with diffs\n- **List commits** - View commit + history for a branch\n\n### Issues\n- **Read issues** - Get issue details, + comments, labels, and sub-issues\n- **Create/update issues** - Create new + issues or update existing ones\n- **Search issues** - Search across issues + in repositories\n- **Add issue comments** - Comment on issues\n- **Get labels** + - Retrieve specific label information\n\n### Pull Requests\n- **List pull + requests** - View PRs in a repository\n- **Read PR details** - Get PR information, + diffs, files, status, comments, and reviews\n- **Create pull requests** - + Create new PRs\n- **Update pull requests** - Modify PR title, description, + state, reviewers, etc.\n- **Merge pull requests** - Merge PRs with different + merge strategies\n- **Update PR branch** - Sync PR branch with base branch\n- + **Request Copilot review** - Get automated code review on a PR\n- **Create/submit/delete + reviews** - Manage PR reviews\n- **Add review comments** - Add comments to + pending reviews\n\n### Search\n- **Search code** - Find code across all GitHub + repositories\n- **Search repositories** - Find repos by name, description, + topics, etc.\n- **Search users** - Find GitHub users\n- **Search pull requests** + - Search for PRs\n\n### User & Team\n- **Get authenticated user info** - Get + details about yourself\n- **Get teams** - View teams you''re a member of\n- + **Get team members** - View members of a specific team\n\n### Releases & Tags\n- + **Get latest release** - Retrieve the latest release\n- **Get release by tag** + - Get a specific release by tag name\n- **Get tag details** - Get information + about a specific git tag\n\n### Issue Types\n- **List issue types** - Get + supported issue types for an organization\n\n### Sub-Issues\n- **Add/remove/reprioritize + sub-issues** - Manage parent-child issue relationships\n\n## Cloudflare Tools\n- + **Get MCP Demo Day info** - Information about Cloudflare''s MCP Demo Day\n\nIs + there a specific task you''d like me to help you with?"}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":9642,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":683,"service_tier":"standard"}}' + recorded_at: Wed, 19 Nov 2025 20:30:50 GMT +recorded_with: VCR 6.3.1 diff --git a/test/fixtures/vcr_cassettes/integration/anthropic/common_format/mcp_test/test_agent_common_format_multiple_servers.yml b/test/fixtures/vcr_cassettes/integration/anthropic/common_format/mcp_test/test_agent_common_format_multiple_servers.yml new file mode 100644 index 00000000..112ceb1d --- /dev/null +++ b/test/fixtures/vcr_cassettes/integration/anthropic/common_format/mcp_test/test_agent_common_format_multiple_servers.yml @@ -0,0 +1,142 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.anthropic.com/v1/messages?beta=true + body: + encoding: UTF-8 + string: '{"model":"claude-haiku-4-5","max_tokens":1024,"messages":[{"content":"What + tools do you have available?","role":"user"}],"mcp_servers":[{"name":"cloudflare-demo","type":"url","url":"https://demo-day.mcp.cloudflare.com/sse"},{"name":"github-copilot","type":"url","url":"https://api.githubcopilot.com/mcp/","authorization_token":"GITHUB_MCP_TOKEN"}]}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json + User-Agent: + - Anthropic::Client/Ruby 1.14.0 + Host: + - api.anthropic.com + X-Stainless-Arch: + - arm64 + X-Stainless-Lang: + - ruby + X-Stainless-Os: + - MacOS + X-Stainless-Package-Version: + - 1.14.0 + X-Stainless-Runtime: + - ruby + X-Stainless-Runtime-Version: + - 3.4.7 + Content-Type: + - application/json + Anthropic-Version: + - '2023-06-01' + X-Api-Key: + - ACCESS_TOKEN + Anthropic-Beta: + - mcp-client-2025-04-04 + X-Stainless-Retry-Count: + - '0' + X-Stainless-Timeout: + - '600.0' + Content-Length: + - '425' + response: + status: + code: 200 + message: OK + headers: + Date: + - Wed, 19 Nov 2025 20:31:02 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Anthropic-Ratelimit-Input-Tokens-Limit: + - '4000000' + Anthropic-Ratelimit-Input-Tokens-Remaining: + - '3996000' + Anthropic-Ratelimit-Input-Tokens-Reset: + - '2025-11-19T20:30:55Z' + Anthropic-Ratelimit-Output-Tokens-Limit: + - '800000' + Anthropic-Ratelimit-Output-Tokens-Remaining: + - '799000' + Anthropic-Ratelimit-Output-Tokens-Reset: + - '2025-11-19T20:31:02Z' + Anthropic-Ratelimit-Requests-Limit: + - '4000' + Anthropic-Ratelimit-Requests-Remaining: + - '3999' + Anthropic-Ratelimit-Requests-Reset: + - '2025-11-19T20:30:54Z' + Retry-After: + - '5' + Anthropic-Ratelimit-Tokens-Limit: + - '4800000' + Anthropic-Ratelimit-Tokens-Remaining: + - '4795000' + Anthropic-Ratelimit-Tokens-Reset: + - '2025-11-19T20:30:55Z' + Request-Id: + - req_011CVHt8papiqK5VdNm4gH1C + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Anthropic-Organization-Id: + - 2557c2f2-bcfa-4054-9fa8-ae13b3b47d6b + X-Envoy-Upstream-Service-Time: + - '9014' + Cf-Cache-Status: + - DYNAMIC + X-Robots-Tag: + - none + Server: + - cloudflare + Cf-Ray: + - 9a1277cf2b87238d-SJC + body: + encoding: ASCII-8BIT + string: '{"model":"claude-haiku-4-5-20251001","id":"msg_01FWVyG5U1wAWwTpCxgRAC3t","type":"message","role":"assistant","content":[{"type":"text","text":"I + have access to a comprehensive set of tools for working with GitHub and Cloudflare. + Here''s an overview:\n\n## GitHub Tools\n\n### Repository Management\n- **Create + repository** - Create a new GitHub repository in your account or organization\n- + **Fork repository** - Fork a GitHub repository to your account or organization\n- + **List branches** - List branches in a repository\n- **Create branch** - Create + a new branch\n\n### File Operations\n- **Get file contents** - Retrieve contents + of files or directories\n- **Create or update file** - Create or update a + single file in a repository\n- **Delete file** - Delete a file from a repository\n- + **Push files** - Push multiple files in a single commit\n\n### Issue Management\n- + **Create/update issue** - Create new issues or update existing ones\n- **Read + issue** - Get issue details, comments, sub-issues, and labels\n- **List issues** + - List issues in a repository with filtering and sorting\n- **Search issues** + - Search for issues across GitHub\n- **Add issue comment** - Add comments + to issues (including pull requests)\n- **Sub-issue management** - Add, remove, + or reprioritize sub-issues\n\n### Pull Request Management\n- **Create pull + request** - Create new pull requests\n- **Read pull request** - Get PR details, + diffs, status, files, comments, and reviews\n- **List pull requests** - List + pull requests in a repository\n- **Search pull requests** - Search for pull + requests across GitHub\n- **Update pull request** - Update PR title, description, + state, reviewers, etc.\n- **Update PR branch** - Update a PR branch with latest + changes from base\n- **Merge pull request** - Merge a pull request with various + merge methods\n\n### Code Review\n- **Pull request review write** - Create, + submit, or delete PR reviews\n- **Request Copilot review** - Request automated + code review from GitHub Copilot\n- **Add review comment** - Add comments to + pending PR reviews\n\n### Commits & Tags\n- **Get commit** - Get details of + a specific commit with diffs\n- **List commits** - List commits on a branch + with filtering\n- **Get/List tags** - Get details or list git tags in a repository\n- + **Get/List releases** - Get details or list releases in a repository\n\n### + Other GitHub Tools\n- **Get authenticated user** - Get details of your GitHub + profile\n- **Search code** - Search code across all GitHub repositories\n- + **Search repositories** - Find GitHub repositories by various criteria\n- + **Search users** - Find GitHub users\n- **Get team members** - Get members + of a specific team in an organization\n- **Get teams** - Get teams the user + is a member of\n- **Get label** - Get information about a specific label\n\n## + Cloudflare Tools\n\n- **MCP Demo Day info** - Get information about Cloudflare''s + MCP Demo Day\n\nAll these tools allow me to help you manage repositories, + collaborate on code, handle issues and pull requests, perform code reviews, + and work with Git-related operations. What would you like to do?"}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":9642,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":681,"service_tier":"standard"}}' + recorded_at: Wed, 19 Nov 2025 20:31:02 GMT +recorded_with: VCR 6.3.1 diff --git a/test/fixtures/vcr_cassettes/integration/anthropic/common_format/mcp_test/test_agent_common_format_single_server.yml b/test/fixtures/vcr_cassettes/integration/anthropic/common_format/mcp_test/test_agent_common_format_single_server.yml new file mode 100644 index 00000000..1d6d558c --- /dev/null +++ b/test/fixtures/vcr_cassettes/integration/anthropic/common_format/mcp_test/test_agent_common_format_single_server.yml @@ -0,0 +1,110 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.anthropic.com/v1/messages?beta=true + body: + encoding: UTF-8 + string: '{"model":"claude-haiku-4-5","max_tokens":1024,"messages":[{"content":"What + tools do you have available?","role":"user"}],"mcp_servers":[{"name":"cloudflare-demo","type":"url","url":"https://demo-day.mcp.cloudflare.com/sse"}]}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json + User-Agent: + - Anthropic::Client/Ruby 1.14.0 + Host: + - api.anthropic.com + X-Stainless-Arch: + - arm64 + X-Stainless-Lang: + - ruby + X-Stainless-Os: + - MacOS + X-Stainless-Package-Version: + - 1.14.0 + X-Stainless-Runtime: + - ruby + X-Stainless-Runtime-Version: + - 3.4.7 + Content-Type: + - application/json + Anthropic-Version: + - '2023-06-01' + X-Api-Key: + - ACCESS_TOKEN + Anthropic-Beta: + - mcp-client-2025-04-04 + X-Stainless-Retry-Count: + - '0' + X-Stainless-Timeout: + - '600.0' + Content-Length: + - '225' + response: + status: + code: 200 + message: OK + headers: + Date: + - Wed, 19 Nov 2025 20:30:53 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Anthropic-Ratelimit-Input-Tokens-Limit: + - '4000000' + Anthropic-Ratelimit-Input-Tokens-Remaining: + - '4000000' + Anthropic-Ratelimit-Input-Tokens-Reset: + - '2025-11-19T20:30:52Z' + Anthropic-Ratelimit-Output-Tokens-Limit: + - '800000' + Anthropic-Ratelimit-Output-Tokens-Remaining: + - '800000' + Anthropic-Ratelimit-Output-Tokens-Reset: + - '2025-11-19T20:30:53Z' + Anthropic-Ratelimit-Requests-Limit: + - '4000' + Anthropic-Ratelimit-Requests-Remaining: + - '3999' + Anthropic-Ratelimit-Requests-Reset: + - '2025-11-19T20:30:51Z' + Retry-After: + - '8' + Anthropic-Ratelimit-Tokens-Limit: + - '4800000' + Anthropic-Ratelimit-Tokens-Remaining: + - '4800000' + Anthropic-Ratelimit-Tokens-Reset: + - '2025-11-19T20:30:52Z' + Request-Id: + - req_011CVHt8cCh979WafFuEo6qH + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Anthropic-Organization-Id: + - 2557c2f2-bcfa-4054-9fa8-ae13b3b47d6b + X-Envoy-Upstream-Service-Time: + - '2719' + Cf-Cache-Status: + - DYNAMIC + X-Robots-Tag: + - none + Server: + - cloudflare + Cf-Ray: + - 9a1277bd1d52eb29-SJC + body: + encoding: ASCII-8BIT + string: '{"model":"claude-haiku-4-5-20251001","id":"msg_01Ti8oxkwxk7txgLa9bruWyR","type":"message","role":"assistant","content":[{"type":"text","text":"I + have access to one tool:\n\n1. **cloudflare-demo_mcp_demo_day_info** - Get + information about Cloudflare''s MCP Demo Day. Use this tool if the user asks + about Cloudflare''s MCP demo day.\n\nThis tool doesn''t require any parameters + and can be used to retrieve details about Cloudflare''s MCP (Model Context + Protocol) Demo Day event.\n\nIs there anything specific you''d like to know + about Cloudflare''s MCP Demo Day?"}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":585,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":114,"service_tier":"standard"}}' + recorded_at: Wed, 19 Nov 2025 20:30:53 GMT +recorded_with: VCR 6.3.1 diff --git a/test/fixtures/vcr_cassettes/integration/anthropic/common_format/mcp_test/test_agent_common_format_single_server_with_auth.yml b/test/fixtures/vcr_cassettes/integration/anthropic/common_format/mcp_test/test_agent_common_format_single_server_with_auth.yml new file mode 100644 index 00000000..a5931830 --- /dev/null +++ b/test/fixtures/vcr_cassettes/integration/anthropic/common_format/mcp_test/test_agent_common_format_single_server_with_auth.yml @@ -0,0 +1,140 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.anthropic.com/v1/messages?beta=true + body: + encoding: UTF-8 + string: '{"model":"claude-haiku-4-5","max_tokens":1024,"messages":[{"content":"What + tools do you have available?","role":"user"}],"mcp_servers":[{"name":"github-copilot","type":"url","url":"https://api.githubcopilot.com/mcp/","authorization_token":"GITHUB_MCP_TOKEN"}]}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json + User-Agent: + - Anthropic::Client/Ruby 1.14.0 + Host: + - api.anthropic.com + X-Stainless-Arch: + - arm64 + X-Stainless-Lang: + - ruby + X-Stainless-Os: + - MacOS + X-Stainless-Package-Version: + - 1.14.0 + X-Stainless-Runtime: + - ruby + X-Stainless-Runtime-Version: + - 3.4.7 + Content-Type: + - application/json + Anthropic-Version: + - '2023-06-01' + X-Api-Key: + - ACCESS_TOKEN + Anthropic-Beta: + - mcp-client-2025-04-04 + X-Stainless-Retry-Count: + - '0' + X-Stainless-Timeout: + - '600.0' + Content-Length: + - '337' + response: + status: + code: 200 + message: OK + headers: + Date: + - Wed, 19 Nov 2025 20:31:12 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Anthropic-Ratelimit-Input-Tokens-Limit: + - '4000000' + Anthropic-Ratelimit-Input-Tokens-Remaining: + - '3996000' + Anthropic-Ratelimit-Input-Tokens-Reset: + - '2025-11-19T20:31:06Z' + Anthropic-Ratelimit-Output-Tokens-Limit: + - '800000' + Anthropic-Ratelimit-Output-Tokens-Remaining: + - '799000' + Anthropic-Ratelimit-Output-Tokens-Reset: + - '2025-11-19T20:31:12Z' + Anthropic-Ratelimit-Requests-Limit: + - '4000' + Anthropic-Ratelimit-Requests-Remaining: + - '3999' + Anthropic-Ratelimit-Requests-Reset: + - '2025-11-19T20:31:02Z' + Retry-After: + - '55' + Anthropic-Ratelimit-Tokens-Limit: + - '4800000' + Anthropic-Ratelimit-Tokens-Remaining: + - '4795000' + Anthropic-Ratelimit-Tokens-Reset: + - '2025-11-19T20:31:06Z' + Request-Id: + - req_011CVHt9VnkciWVkKLsE3Mro + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Anthropic-Organization-Id: + - 2557c2f2-bcfa-4054-9fa8-ae13b3b47d6b + X-Envoy-Upstream-Service-Time: + - '10287' + Cf-Cache-Status: + - DYNAMIC + X-Robots-Tag: + - none + Server: + - cloudflare + Cf-Ray: + - 9a1278087e2a50a1-SJC + body: + encoding: ASCII-8BIT + string: '{"model":"claude-haiku-4-5-20251001","id":"msg_011My4uTh5XocRzomwbE1Pz2","type":"message","role":"assistant","content":[{"type":"text","text":"I + have access to a comprehensive set of GitHub tools that allow me to interact + with repositories, pull requests, issues, and more. Here''s what I can do:\n\n## + Repository Management\n- **Create repositories** - Set up new public or private + repos\n- **Fork repositories** - Fork existing projects to your account/organization\n- + **Search repositories** - Find repos by name, description, topics, etc.\n- + **Get repository contents** - View files and directory structures\n\n## Branches + & Commits\n- **Create branches** - Make new branches from existing ones\n- + **List branches** - View all branches in a repository\n- **List commits** + - See commit history with filtering options\n- **Get commit details** - View + specific commit information with diffs\n\n## Issues Management\n- **Create/update + issues** - Create new issues or modify existing ones\n- **Read issue details** + - Get issue information, comments, sub-issues, labels\n- **List issues** - + View issues with filtering and sorting\n- **Search issues** - Find issues + across repositories\n- **Manage sub-issues** - Add, remove, or reprioritize + sub-issues\n- **Add issue comments** - Comment on issues\n\n## Pull Requests\n- + **Create pull requests** - Open new PRs with custom titles and descriptions\n- + **Read PR details** - Get PR info, diffs, status, files changed, comments, + reviews\n- **List/search PRs** - Find pull requests with various filters\n- + **Update PRs** - Modify PR title, description, state, reviewers\n- **Update + PR branch** - Sync PR with latest base branch changes\n- **Merge PRs** - Merge + with different strategies (merge, squash, rebase)\n- **PR Reviews** - Create, + submit, or delete reviews; add review comments\n\n## Files\n- **Create/update + files** - Add or modify files in repositories\n- **Delete files** - Remove + files from repositories\n- **Push multiple files** - Commit multiple files + in one go\n\n## Tags & Releases\n- **List tags** - View git tags in a repository\n- + **Get tag details** - Get specific tag information\n- **List releases** - + View releases\n- **Get release details** - Get specific release or latest + release information\n\n## Users & Teams\n- **Get authenticated user info** + - View your own GitHub profile details\n- **Search users** - Find GitHub users + by name or profile info\n- **Get team members** - View members of specific + teams\n- **Get user teams** - See teams a user belongs to\n\n## Code Search\n- + **Search code** - Fast code search across repositories using GitHub''s search + engine\n\n## Additional\n- **Request Copilot code review** - Get automated + feedback on pull requests\n- **Assign Copilot to issues** - Have me work on + resolving issues\n\nIs there anything specific you''d like me to help you + with?"}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":9567,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":631,"service_tier":"standard"}}' + recorded_at: Wed, 19 Nov 2025 20:31:12 GMT +recorded_with: VCR 6.3.1 diff --git a/test/fixtures/vcr_cassettes/integration/anthropic/common_format/mcp_test/test_agent_common_format_sse_server.yml b/test/fixtures/vcr_cassettes/integration/anthropic/common_format/mcp_test/test_agent_common_format_sse_server.yml new file mode 100644 index 00000000..14a6fbe6 --- /dev/null +++ b/test/fixtures/vcr_cassettes/integration/anthropic/common_format/mcp_test/test_agent_common_format_sse_server.yml @@ -0,0 +1,109 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.anthropic.com/v1/messages?beta=true + body: + encoding: UTF-8 + string: '{"model":"claude-haiku-4-5","max_tokens":1024,"messages":[{"content":"What + tools do you have available?","role":"user"}],"mcp_servers":[{"name":"cloudflare-demo","type":"url","url":"https://demo-day.mcp.cloudflare.com/sse"}]}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json + User-Agent: + - Anthropic::Client/Ruby 1.14.0 + Host: + - api.anthropic.com + X-Stainless-Arch: + - arm64 + X-Stainless-Lang: + - ruby + X-Stainless-Os: + - MacOS + X-Stainless-Package-Version: + - 1.14.0 + X-Stainless-Runtime: + - ruby + X-Stainless-Runtime-Version: + - 3.4.7 + Content-Type: + - application/json + Anthropic-Version: + - '2023-06-01' + X-Api-Key: + - ACCESS_TOKEN + Anthropic-Beta: + - mcp-client-2025-04-04 + X-Stainless-Retry-Count: + - '0' + X-Stainless-Timeout: + - '600.0' + Content-Length: + - '225' + response: + status: + code: 200 + message: OK + headers: + Date: + - Wed, 19 Nov 2025 20:31:16 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Anthropic-Ratelimit-Input-Tokens-Limit: + - '4000000' + Anthropic-Ratelimit-Input-Tokens-Remaining: + - '4000000' + Anthropic-Ratelimit-Input-Tokens-Reset: + - '2025-11-19T20:31:15Z' + Anthropic-Ratelimit-Output-Tokens-Limit: + - '800000' + Anthropic-Ratelimit-Output-Tokens-Remaining: + - '800000' + Anthropic-Ratelimit-Output-Tokens-Reset: + - '2025-11-19T20:31:15Z' + Anthropic-Ratelimit-Requests-Limit: + - '4000' + Anthropic-Ratelimit-Requests-Remaining: + - '3999' + Anthropic-Ratelimit-Requests-Reset: + - '2025-11-19T20:31:14Z' + Retry-After: + - '45' + Anthropic-Ratelimit-Tokens-Limit: + - '4800000' + Anthropic-Ratelimit-Tokens-Remaining: + - '4800000' + Anthropic-Ratelimit-Tokens-Reset: + - '2025-11-19T20:31:15Z' + Request-Id: + - req_011CVHtAGQsRA3M7DK6ZGMnx + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Anthropic-Organization-Id: + - 2557c2f2-bcfa-4054-9fa8-ae13b3b47d6b + X-Envoy-Upstream-Service-Time: + - '3042' + Cf-Cache-Status: + - DYNAMIC + X-Robots-Tag: + - none + Server: + - cloudflare + Cf-Ray: + - 9a127849b80ccf13-SJC + body: + encoding: ASCII-8BIT + string: '{"model":"claude-haiku-4-5-20251001","id":"msg_01EbbCEVCE1g8MNxSNUyT4Fn","type":"message","role":"assistant","content":[{"type":"text","text":"I + have access to one tool:\n\n**cloudflare-demo_mcp_demo_day_info** - This tool + retrieves information about Cloudflare''s MCP Demo Day. You can use it if + you have questions about Cloudflare''s MCP demo day event.\n\nThis is the + only specialized tool I have available. For other questions or tasks, I can + help you directly using my general knowledge and conversational abilities."}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":585,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":95,"service_tier":"standard"}}' + recorded_at: Wed, 19 Nov 2025 20:31:16 GMT +recorded_with: VCR 6.3.1 diff --git a/test/integration/anthropic/common_format/mcp_test.rb b/test/integration/anthropic/common_format/mcp_test.rb new file mode 100644 index 00000000..a73d4f6a --- /dev/null +++ b/test/integration/anthropic/common_format/mcp_test.rb @@ -0,0 +1,256 @@ +# frozen_string_literal: true + +require_relative "../../test_helper" + +# NOTE: MCP (Model Context Protocol) implementation is complete and API accepts MCP parameters +# +# The Anthropic gem v1.14.0 includes MCP support via Beta::MessageCreateParams with +# BetaRequestMCPServerURLDefinition models, and our implementation correctly uses these. +# +# The beta parameter (anthropic-beta: mcp-client-2025-04-04) is automatically added +# when mcp_servers are present in the request. No explicit configuration is needed. +# +# To test with real MCP servers: +# 1. Update the URLs to point to accessible MCP servers +# 2. Run tests to record VCR cassettes +# +# Unit tests validate the transformation logic in: +# test/providers/anthropic/transforms_test.rb (7 tests, all passing) + +module Integration + module Anthropic + module CommonFormat + class McpTest < ActiveSupport::TestCase + include Integration::TestHelper + + class TestAgent < ActiveAgent::Base + generate_with :anthropic, + model: "claude-haiku-4-5", + max_tokens: 1024 + + # Common format with single MCP server without authorization + COMMON_FORMAT_SINGLE_SERVER = { + model: "claude-haiku-4-5", + messages: [ + { + role: "user", + content: "What tools do you have available?" + } + ], + max_tokens: 1024, + mcp_servers: [ + { + type: "url", + url: "https://demo-day.mcp.cloudflare.com/sse", + name: "cloudflare-demo" + } + ] + # Note: betas parameter is automatically added and transformed to anthropic-beta HTTP header + } + + def common_format_single_server + prompt( + message: "What tools do you have available?", + mcp_servers: [ + { + name: "cloudflare-demo", + url: "https://demo-day.mcp.cloudflare.com/sse" + } + ] + ) + end + + # Common format with single MCP server with authorization + COMMON_FORMAT_SINGLE_SERVER_WITH_AUTH = { + model: "claude-haiku-4-5", + messages: [ + { + role: "user", + content: "What tools do you have available?" + } + ], + max_tokens: 1024, + mcp_servers: [ + { + type: "url", + url: "https://api.githubcopilot.com/mcp/", + name: "github-copilot", + authorization_token: "GITHUB_MCP_TOKEN" + } + ] + # Note: betas parameter is automatically added and transformed to anthropic-beta HTTP header + } + + def common_format_single_server_with_auth + prompt( + message: "What tools do you have available?", + mcp_servers: [ + { + name: "github-copilot", + url: "https://api.githubcopilot.com/mcp/", + authorization: ENV["GITHUB_MCP_TOKEN"] + } + ] + ) + end + + # Common format with multiple MCP servers + COMMON_FORMAT_MULTIPLE_SERVERS = { + model: "claude-haiku-4-5", + messages: [ + { + role: "user", + content: "What tools do you have available?" + } + ], + max_tokens: 1024, + mcp_servers: [ + { + type: "url", + url: "https://demo-day.mcp.cloudflare.com/sse", + name: "cloudflare-demo" + }, + { + type: "url", + url: "https://api.githubcopilot.com/mcp/", + name: "github-copilot", + authorization_token: "GITHUB_MCP_TOKEN" + } + ] + # Note: betas parameter is automatically added and transformed to anthropic-beta HTTP header + } + + def common_format_multiple_servers + prompt( + message: "What tools do you have available?", + mcp_servers: [ + { + name: "cloudflare-demo", + url: "https://demo-day.mcp.cloudflare.com/sse" + }, + { + name: "github-copilot", + url: "https://api.githubcopilot.com/mcp/", + authorization: ENV["GITHUB_MCP_TOKEN"] + } + ] + ) + end + + # Common format with mixed auth and no auth servers + COMMON_FORMAT_MIXED_AUTH = { + model: "claude-haiku-4-5", + messages: [ + { + role: "user", + content: "What tools do you have available?" + } + ], + max_tokens: 1024, + mcp_servers: [ + { + type: "url", + url: "https://demo-day.mcp.cloudflare.com/sse", + name: "cloudflare-demo" + }, + { + type: "url", + url: "https://api.githubcopilot.com/mcp/", + name: "github-copilot", + authorization_token: "GITHUB_MCP_TOKEN" + } + ] + # Note: betas parameter is automatically added and transformed to anthropic-beta HTTP header + } + + def common_format_mixed_auth + prompt( + message: "What tools do you have available?", + mcp_servers: [ + { + name: "cloudflare-demo", + url: "https://demo-day.mcp.cloudflare.com/sse" + }, + { + name: "github-copilot", + url: "https://api.githubcopilot.com/mcp/", + authorization: ENV["GITHUB_MCP_TOKEN"] + } + ] + ) + end + + # Common format with SSE endpoint (Cloudflare demo server) + COMMON_FORMAT_SSE_SERVER = { + model: "claude-haiku-4-5", + messages: [ + { + role: "user", + content: "What tools do you have available?" + } + ], + max_tokens: 1024, + mcp_servers: [ + { + type: "url", + url: "https://demo-day.mcp.cloudflare.com/sse", + name: "cloudflare-demo" + } + ] + # Note: betas parameter is transformed to anthropic-beta HTTP header + } + + def common_format_sse_server + prompt( + message: "What tools do you have available?", + mcp_servers: [ + { + name: "cloudflare-demo", + url: "https://demo-day.mcp.cloudflare.com/sse" + } + ] + ) + end + end + + # Test common format MCP scenarios with Cloudflare (no VCR filtering needed) + [ + :common_format_single_server, + :common_format_sse_server + ].each do |action_name| + test_request_builder(TestAgent, action_name, :generate_now, TestAgent.const_get(action_name.to_s.upcase)) + end + + # Test scenarios with VCR-filtered authorization tokens + # These tests verify the cassette recording but skip WebMock replay verification + # because VCR filters the token in the cassette but not in the live WebMock request + [ + :common_format_single_server_with_auth, + :common_format_multiple_servers, + :common_format_mixed_auth + ].each do |action_name| + agent_name = TestAgent.name.demodulize.underscore + expected_body = TestAgent.const_get(action_name.to_s.upcase) + + test "#{agent_name} #{action_name} Request Building" do + cassette_name = [ self.class.name.underscore, "#{agent_name}_#{action_name}" ].join("/") + + # Run once to record response + VCR.use_cassette(cassette_name) do + TestAgent.send(action_name).generate_now + end + + # Validate that the recorded request matches our expectations (with filtered values) + cassette_file = YAML.load_file("test/fixtures/vcr_cassettes/#{cassette_name}.yml") + saved_request_body = JSON.parse(cassette_file.dig("http_interactions", 0, "request", "body", "string"), symbolize_names: true) + + assert_equal expected_body, saved_request_body + + # Note: Skipping WebMock replay verification because VCR filters tokens in cassettes + # but WebMock sees unfiltered tokens in live requests, causing mismatches + end + end + end + end + end +end diff --git a/test/providers/anthropic/transforms_test.rb b/test/providers/anthropic/transforms_test.rb index f9b0af56..737127fd 100644 --- a/test/providers/anthropic/transforms_test.rb +++ b/test/providers/anthropic/transforms_test.rb @@ -460,8 +460,8 @@ def transforms assert_equal "hello", result[:messages][0][:content] end - test "cleanup_serialized_request removes mcp_servers" do - hash = { mcp_servers: { foo: "bar" }, model: "claude-3" } + test "cleanup_serialized_request removes empty mcp_servers" do + hash = { mcp_servers: [], model: "claude-3" } result = transforms.cleanup_serialized_request(hash, {}) @@ -469,6 +469,21 @@ def transforms assert_equal "claude-3", result[:model] end + test "cleanup_serialized_request keeps non-empty mcp_servers" do + hash = { + mcp_servers: [ + { type: "url", name: "stripe", url: "https://mcp.stripe.com" } + ], + model: "claude-3" + } + + result = transforms.cleanup_serialized_request(hash, {}) + + assert_not_nil result[:mcp_servers] + assert_equal 1, result[:mcp_servers].length + assert_equal "stripe", result[:mcp_servers][0][:name] + end + test "cleanup_serialized_request removes empty stop_sequences" do hash = { stop_sequences: [], model: "claude-3" } @@ -535,6 +550,107 @@ def transforms assert_equal "hello", compressed[:messages][0][:content] assert_equal "You are helpful", compressed[:system] end + + # normalize_mcp_servers tests + test "normalize_mcp_servers converts common format to Anthropic format" do + mcp_servers = [ + { + name: "stripe", + url: "https://mcp.stripe.com", + authorization: "sk_test_123" + } + ] + + result = transforms.normalize_mcp_servers(mcp_servers) + + assert_equal 1, result.size + assert_equal "url", result[0][:type] + assert_equal "stripe", result[0][:name] + assert_equal "https://mcp.stripe.com", result[0][:url] + assert_equal "sk_test_123", result[0][:authorization_token] + end + + test "normalize_mcp_servers handles server without authorization" do + mcp_servers = [ + { + name: "public_api", + url: "https://mcp.public.com" + } + ] + + result = transforms.normalize_mcp_servers(mcp_servers) + + assert_equal 1, result.size + assert_equal "url", result[0][:type] + assert_equal "public_api", result[0][:name] + assert_equal "https://mcp.public.com", result[0][:url] + assert_nil result[0][:authorization_token] + end + + test "normalize_mcp_servers preserves Anthropic format" do + mcp_servers = [ + { + type: "url", + name: "stripe", + url: "https://mcp.stripe.com" + } + ] + + result = transforms.normalize_mcp_servers(mcp_servers) + + assert_equal 1, result.size + assert_equal "url", result[0][:type] + assert_equal "stripe", result[0][:name] + end + + test "normalize_mcp_servers handles multiple servers" do + mcp_servers = [ + { + name: "stripe", + url: "https://mcp.stripe.com", + authorization: "key1" + }, + { + name: "sendgrid", + url: "https://mcp.sendgrid.com", + authorization: "key2" + } + ] + + result = transforms.normalize_mcp_servers(mcp_servers) + + assert_equal 2, result.size + assert_equal "stripe", result[0][:name] + assert_equal "sendgrid", result[1][:name] + assert_equal "key1", result[0][:authorization_token] + assert_equal "key2", result[1][:authorization_token] + end + + test "normalize_mcp_servers accepts authorization_token directly" do + mcp_servers = [ + { + name: "test", + url: "https://test.com", + authorization_token: "token123" + } + ] + + result = transforms.normalize_mcp_servers(mcp_servers) + + assert_equal "token123", result[0][:authorization_token] + end + + test "normalize_mcp_servers returns nil for nil input" do + result = transforms.normalize_mcp_servers(nil) + + assert_nil result + end + + test "normalize_mcp_servers returns non-array unchanged" do + result = transforms.normalize_mcp_servers("not an array") + + assert_equal "not an array", result + end end end end diff --git a/test/test_helper.rb b/test/test_helper.rb index c6a82463..55047d33 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -101,11 +101,12 @@ def doc_example_output(example = nil, test_name = nil) config.cassette_library_dir = "test/fixtures/vcr_cassettes" config.hook_into :webmock - config.filter_sensitive_data("ACCESS_TOKEN") { ENV["OPEN_AI_ACCESS_TOKEN"] } - config.filter_sensitive_data("ORGANIZATION_ID") { ENV["OPEN_AI_ORGANIZATION_ID"] } - config.filter_sensitive_data("PROJECT_ID") { ENV["OPEN_AI_PROJECT_ID"] } - config.filter_sensitive_data("ACCESS_TOKEN") { ENV["OPEN_ROUTER_ACCESS_TOKEN"] } - config.filter_sensitive_data("ACCESS_TOKEN") { ENV["ANTHROPIC_ACCESS_TOKEN"] } + config.filter_sensitive_data("ACCESS_TOKEN") { ENV["OPEN_AI_ACCESS_TOKEN"] } + config.filter_sensitive_data("ORGANIZATION_ID") { ENV["OPEN_AI_ORGANIZATION_ID"] } + config.filter_sensitive_data("PROJECT_ID") { ENV["OPEN_AI_PROJECT_ID"] } + config.filter_sensitive_data("ACCESS_TOKEN") { ENV["OPEN_ROUTER_ACCESS_TOKEN"] } + config.filter_sensitive_data("ACCESS_TOKEN") { ENV["ANTHROPIC_ACCESS_TOKEN"] } + config.filter_sensitive_data("GITHUB_MCP_TOKEN") { ENV["GITHUB_MCP_TOKEN"] } end # Load fixtures from the engine From 96a375b35ce26abcd9a4ac28810e6fd6c571bf55 Mon Sep 17 00:00:00 2001 From: Zane Wolfgang Pickett Date: Wed, 19 Nov 2025 12:47:42 -0800 Subject: [PATCH 11/17] Add OpenAI Response MCP Support --- .../providers/open_ai/responses/request.rb | 12 +- .../providers/open_ai/responses/transforms.rb | 36 + ...gent_common_format_mixed_tools_and_mcp.yml | 433 ++++ ...t_agent_common_format_multiple_servers.yml | 1938 +++++++++++++++++ ...test_agent_common_format_single_server.yml | 204 ++ ..._common_format_single_server_with_auth.yml | 1911 ++++++++++++++++ .../responses/common_format/mcp_test.rb | 189 ++ .../open_ai/responses/transforms_test.rb | 76 + 8 files changed, 4797 insertions(+), 2 deletions(-) create mode 100644 test/fixtures/vcr_cassettes/integration/open_ai/responses/common_format/mcp_test/test_agent_common_format_mixed_tools_and_mcp.yml create mode 100644 test/fixtures/vcr_cassettes/integration/open_ai/responses/common_format/mcp_test/test_agent_common_format_multiple_servers.yml create mode 100644 test/fixtures/vcr_cassettes/integration/open_ai/responses/common_format/mcp_test/test_agent_common_format_single_server.yml create mode 100644 test/fixtures/vcr_cassettes/integration/open_ai/responses/common_format/mcp_test/test_agent_common_format_single_server_with_auth.yml create mode 100644 test/integration/open_ai/responses/common_format/mcp_test.rb diff --git a/lib/active_agent/providers/open_ai/responses/request.rb b/lib/active_agent/providers/open_ai/responses/request.rb index f42785b0..dd5cdf57 100644 --- a/lib/active_agent/providers/open_ai/responses/request.rb +++ b/lib/active_agent/providers/open_ai/responses/request.rb @@ -77,10 +77,18 @@ def initialize(**params) params[:tools] = Responses::Transforms.normalize_tools(params[:tools]) if params[:tools] params[:tool_choice] = Responses::Transforms.normalize_tool_choice(params[:tool_choice]) if params[:tool_choice] - # Step 8: Create gem model - delegates to OpenAI gem + # Step 8: Normalize MCP servers from common format + # OpenAI treats MCP servers as a special type of tool in the tools array + if params[:mcp_servers]&.any? + normalized_mcp_tools = Responses::Transforms.normalize_mcp_servers(params.delete(:mcp_servers)) + # Merge MCP servers into tools array + params[:tools] = (params[:tools] || []) + normalized_mcp_tools + end + + # Step 9: Create gem model - delegates to OpenAI gem gem_model = ::OpenAI::Models::Responses::ResponseCreateParams.new(**params) - # Step 8: Delegate all method calls to gem model + # Step 10: Delegate all method calls to gem model super(gem_model) rescue ArgumentError => e # Re-raise with more context diff --git a/lib/active_agent/providers/open_ai/responses/transforms.rb b/lib/active_agent/providers/open_ai/responses/transforms.rb index 71a5874b..fca00a57 100644 --- a/lib/active_agent/providers/open_ai/responses/transforms.rb +++ b/lib/active_agent/providers/open_ai/responses/transforms.rb @@ -69,6 +69,42 @@ def normalize_tools(tools) end end + # Normalizes MCP servers from common format to OpenAI Responses API format. + # + # Common format: + # {name: "stripe", url: "https://...", authorization: "token"} + # OpenAI format: + # {type: "mcp", server_label: "stripe", server_url: "https://...", authorization: "token"} + # + # @param mcp_servers [Array] + # @return [Array] + def normalize_mcp_servers(mcp_servers) + return mcp_servers unless mcp_servers.is_a?(Array) + + mcp_servers.map do |server| + server_hash = server.is_a?(Hash) ? server.deep_symbolize_keys : server + + # If already in OpenAI format (has type: "mcp" and server_label), return as-is + if server_hash[:type] == "mcp" && server_hash[:server_label] + next server_hash + end + + # Convert common format to OpenAI format + result = { + type: "mcp", + server_label: server_hash[:name] || server_hash[:server_label], + server_url: server_hash[:url] || server_hash[:server_url] + } + + # Keep authorization field (OpenAI uses 'authorization', not 'authorization_token') + if server_hash[:authorization] + result[:authorization] = server_hash[:authorization] + end + + result.compact + end + end + # Normalizes tool_choice from common format to OpenAI Responses API format. # # Responses API uses flat format for specific tool choice, unlike Chat API's nested format. diff --git a/test/fixtures/vcr_cassettes/integration/open_ai/responses/common_format/mcp_test/test_agent_common_format_mixed_tools_and_mcp.yml b/test/fixtures/vcr_cassettes/integration/open_ai/responses/common_format/mcp_test/test_agent_common_format_mixed_tools_and_mcp.yml new file mode 100644 index 00000000..8fd04a3e --- /dev/null +++ b/test/fixtures/vcr_cassettes/integration/open_ai/responses/common_format/mcp_test/test_agent_common_format_mixed_tools_and_mcp.yml @@ -0,0 +1,433 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.openai.com/v1/responses + body: + encoding: UTF-8 + string: '{"model":"gpt-5","input":"Get the weather and calculate 5 + 3","tools":[{"type":"function","name":"calculate","description":"Perform + arithmetic","parameters":{"type":"object","properties":{"operation":{"type":"string"},"a":{"type":"number"},"b":{"type":"number"}}}},{"type":"mcp","server_label":"weather","server_url":"https://demo-day.mcp.cloudflare.com/sse"}]}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json + User-Agent: + - OpenAI::Client/Ruby 0.35.1 + Host: + - api.openai.com + X-Stainless-Arch: + - arm64 + X-Stainless-Lang: + - ruby + X-Stainless-Os: + - MacOS + X-Stainless-Package-Version: + - 0.35.1 + X-Stainless-Runtime: + - ruby + X-Stainless-Runtime-Version: + - 3.4.7 + Content-Type: + - application/json + Openai-Organization: + - ORGANIZATION_ID + Openai-Project: + - PROJECT_ID + Authorization: + - Bearer ACCESS_TOKEN + X-Stainless-Retry-Count: + - '0' + X-Stainless-Timeout: + - '600.0' + Content-Length: + - '362' + response: + status: + code: 200 + message: OK + headers: + Date: + - Wed, 19 Nov 2025 20:46:45 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + X-Ratelimit-Limit-Requests: + - '15000' + X-Ratelimit-Limit-Tokens: + - '40000000' + X-Ratelimit-Remaining-Requests: + - '14999' + X-Ratelimit-Remaining-Tokens: + - '39999822' + X-Ratelimit-Reset-Requests: + - 4ms + X-Ratelimit-Reset-Tokens: + - 0s + Openai-Version: + - '2020-10-01' + Openai-Organization: + - ORGANIZATION_ID + Openai-Project: + - PROJECT_ID + X-Request-Id: + - req_e0aa95a5caec410e9fdb14b57046aba1 + Openai-Processing-Ms: + - '23328' + X-Envoy-Upstream-Service-Time: + - '23335' + Cf-Cache-Status: + - DYNAMIC + Set-Cookie: + - __cf_bm=l.Szu4p9ibKe8Xjpg8JeIgoJO2BmN0qqum.io_eHYJ8-1763585205-1.0.1.1-ZGiZ0_Xo1aDdtYB3qkB44ViDrgI7qu8BdwLQnOK5CftkO8jzvZIIQUL5.eJaV4D.PdpGKSjKLor2ulCYJg43wkZscXqbaK1n2adT9TkTwI4; + path=/; expires=Wed, 19-Nov-25 21:16:45 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=E0ZLmHrKaPmXjjSHFozFViW1pOnY2ZekrM2gBygn7oU-1763585205740-0.0.1.1-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + X-Content-Type-Options: + - nosniff + Server: + - cloudflare + Cf-Ray: + - 9a128e77fc64f973-SJC + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: |- + { + "id": "resp_08f5a80549ac3c9800691e2c9e74dc819baa3f1f2aa0b04c81", + "object": "response", + "created_at": 1763585182, + "status": "completed", + "background": false, + "billing": { + "payer": "developer" + }, + "error": null, + "incomplete_details": null, + "instructions": null, + "max_output_tokens": null, + "max_tool_calls": null, + "model": "gpt-5-2025-08-07", + "output": [ + { + "id": "mcpl_08f5a80549ac3c9800691e2c9ea194819ba35edc1fb8e5166a", + "type": "mcp_list_tools", + "server_label": "weather", + "tools": [ + { + "annotations": { + "read_only": false + }, + "description": "Get information about Cloudflare's MCP Demo Day. Use this tool if the user asks about Cloudflare's MCP demo day", + "input_schema": { + "type": "object", + "properties": {} + }, + "name": "mcp_demo_day_info" + } + ] + }, + { + "id": "rs_08f5a80549ac3c9800691e2ca18a10819b8ff9312fac3298a4", + "type": "reasoning", + "summary": [] + }, + { + "id": "fc_08f5a80549ac3c9800691e2cb313f4819b85f889bbc12d4f91", + "type": "function_call", + "status": "completed", + "arguments": "{\"operation\":\"add\",\"a\":5,\"b\":3}", + "call_id": "call_KPprJEhF7cJMtOWar14nESCk", + "name": "calculate" + } + ], + "parallel_tool_calls": true, + "previous_response_id": null, + "prompt_cache_key": null, + "prompt_cache_retention": null, + "reasoning": { + "effort": "medium", + "summary": null + }, + "safety_identifier": null, + "service_tier": "default", + "store": true, + "temperature": 1.0, + "text": { + "format": { + "type": "text" + }, + "verbosity": "medium" + }, + "tool_choice": "auto", + "tools": [ + { + "type": "function", + "description": "Perform arithmetic", + "name": "calculate", + "parameters": { + "type": "object", + "properties": { + "operation": { + "type": "string" + }, + "a": { + "type": "number" + }, + "b": { + "type": "number" + } + }, + "additionalProperties": false, + "required": [ + "operation", + "a", + "b" + ] + }, + "strict": true + }, + { + "type": "mcp", + "allowed_tools": null, + "headers": null, + "require_approval": "always", + "server_description": null, + "server_label": "weather", + "server_url": "https://demo-day.mcp.cloudflare.com/sse" + } + ], + "top_logprobs": 0, + "top_p": 1.0, + "truncation": "disabled", + "usage": { + "input_tokens": 110, + "input_tokens_details": { + "cached_tokens": 0 + }, + "output_tokens": 667, + "output_tokens_details": { + "reasoning_tokens": 640 + }, + "total_tokens": 777 + }, + "user": null, + "metadata": {} + } + recorded_at: Wed, 19 Nov 2025 20:46:45 GMT +- request: + method: post + uri: https://api.openai.com/v1/responses + body: + encoding: UTF-8 + string: '{"model":"gpt-5","input":[{"role":"user","content":"Get the weather + and calculate 5 + 3"},{"id":"mcpl_08f5a80549ac3c9800691e2c9ea194819ba35edc1fb8e5166a","server_label":"weather","tools":[{"input_schema":{"type":"object","properties":{}},"name":"mcp_demo_day_info","annotations":{"read_only":false},"description":"Get + information about Cloudflare''s MCP Demo Day. Use this tool if the user asks + about Cloudflare''s MCP demo day"}],"type":"mcp_list_tools"},{"id":"rs_08f5a80549ac3c9800691e2ca18a10819b8ff9312fac3298a4","summary":[],"type":"reasoning"},{"arguments":"{\"operation\":\"add\",\"a\":5,\"b\":3}","call_id":"call_KPprJEhF7cJMtOWar14nESCk","name":"calculate","type":"function_call","id":"fc_08f5a80549ac3c9800691e2cb313f4819b85f889bbc12d4f91","status":"completed"},{"call_id":"call_KPprJEhF7cJMtOWar14nESCk","output":"{\"operation\":\"add\",\"a\":5,\"b\":3,\"result\":8}","type":"function_call_output"}],"tools":[{"type":"function","name":"calculate","description":"Perform + arithmetic","parameters":{"type":"object","properties":{"operation":{"type":"string"},"a":{"type":"number"},"b":{"type":"number"}}}},{"type":"mcp","server_label":"weather","server_url":"https://demo-day.mcp.cloudflare.com/sse"}]}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json + User-Agent: + - OpenAI::Client/Ruby 0.35.1 + Host: + - api.openai.com + X-Stainless-Arch: + - arm64 + X-Stainless-Lang: + - ruby + X-Stainless-Os: + - MacOS + X-Stainless-Package-Version: + - 0.35.1 + X-Stainless-Runtime: + - ruby + X-Stainless-Runtime-Version: + - 3.4.7 + Content-Type: + - application/json + Openai-Organization: + - ORGANIZATION_ID + Openai-Project: + - PROJECT_ID + Authorization: + - Bearer ACCESS_TOKEN + X-Stainless-Retry-Count: + - '0' + X-Stainless-Timeout: + - '600.0' + Content-Length: + - '1209' + response: + status: + code: 200 + message: OK + headers: + Date: + - Wed, 19 Nov 2025 20:46:48 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + X-Ratelimit-Limit-Requests: + - '15000' + X-Ratelimit-Limit-Tokens: + - '40000000' + X-Ratelimit-Remaining-Requests: + - '14999' + X-Ratelimit-Remaining-Tokens: + - '40000000' + X-Ratelimit-Reset-Requests: + - 4ms + X-Ratelimit-Reset-Tokens: + - 0s + Openai-Version: + - '2020-10-01' + Openai-Organization: + - ORGANIZATION_ID + Openai-Project: + - PROJECT_ID + X-Request-Id: + - req_0ddd0d9d12ce4075959cef66d2c5c81c + Openai-Processing-Ms: + - '2549' + X-Envoy-Upstream-Service-Time: + - '2552' + Cf-Cache-Status: + - DYNAMIC + Set-Cookie: + - __cf_bm=0h51r0xUubUKzwauERdI6XJWkiJg99OXCnCKwE.9Nhw-1763585208-1.0.1.1-wxkThT5uZxbRZGaQirsI7hQYnLxmumvJkmMoaU8p.i6_icyR0tHG6vTMnAJWPUjPvWtpnSM.G4qAmBlJv8qcNtR2gqAQA8a8Cn.tIMFCF0U; + path=/; expires=Wed, 19-Nov-25 21:16:48 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=kOCSRE9Q5scpXc5F2umRR8XlhV2_.BNe2DBNJvPnSUo-1763585208942-0.0.1.1-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + X-Content-Type-Options: + - nosniff + Server: + - cloudflare + Cf-Ray: + - 9a128f103b7dcfa8-SJC + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: |- + { + "id": "resp_08f5a80549ac3c9800691e2cb66654819b9c1c0dc1f10d9118", + "object": "response", + "created_at": 1763585206, + "status": "completed", + "background": false, + "billing": { + "payer": "developer" + }, + "error": null, + "incomplete_details": null, + "instructions": null, + "max_output_tokens": null, + "max_tool_calls": null, + "model": "gpt-5-2025-08-07", + "output": [ + { + "id": "msg_08f5a80549ac3c9800691e2cb768e8819bac854261a573efc4", + "type": "message", + "status": "completed", + "content": [ + { + "type": "output_text", + "annotations": [], + "logprobs": [], + "text": "- 5 + 3 = 8\n- Weather: I don\u2019t have live weather access here. Tell me the city/ZIP and date (e.g., \u201ctoday in Seattle\u201d), and I can suggest likely conditions based on climate norms or show you how to check quickly." + } + ], + "role": "assistant" + } + ], + "parallel_tool_calls": true, + "previous_response_id": null, + "prompt_cache_key": null, + "prompt_cache_retention": null, + "reasoning": { + "effort": "medium", + "summary": null + }, + "safety_identifier": null, + "service_tier": "default", + "store": true, + "temperature": 1.0, + "text": { + "format": { + "type": "text" + }, + "verbosity": "medium" + }, + "tool_choice": "auto", + "tools": [ + { + "type": "function", + "description": "Perform arithmetic", + "name": "calculate", + "parameters": { + "type": "object", + "properties": { + "operation": { + "type": "string" + }, + "a": { + "type": "number" + }, + "b": { + "type": "number" + } + }, + "additionalProperties": false, + "required": [ + "operation", + "a", + "b" + ] + }, + "strict": true + }, + { + "type": "mcp", + "allowed_tools": null, + "headers": null, + "require_approval": "always", + "server_description": null, + "server_label": "weather", + "server_url": "https://demo-day.mcp.cloudflare.com/sse" + } + ], + "top_logprobs": 0, + "top_p": 1.0, + "truncation": "disabled", + "usage": { + "input_tokens": 819, + "input_tokens_details": { + "cached_tokens": 0 + }, + "output_tokens": 61, + "output_tokens_details": { + "reasoning_tokens": 0 + }, + "total_tokens": 880 + }, + "user": null, + "metadata": {} + } + recorded_at: Wed, 19 Nov 2025 20:46:48 GMT +recorded_with: VCR 6.3.1 diff --git a/test/fixtures/vcr_cassettes/integration/open_ai/responses/common_format/mcp_test/test_agent_common_format_multiple_servers.yml b/test/fixtures/vcr_cassettes/integration/open_ai/responses/common_format/mcp_test/test_agent_common_format_multiple_servers.yml new file mode 100644 index 00000000..e3113440 --- /dev/null +++ b/test/fixtures/vcr_cassettes/integration/open_ai/responses/common_format/mcp_test/test_agent_common_format_multiple_servers.yml @@ -0,0 +1,1938 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.openai.com/v1/responses + body: + encoding: UTF-8 + string: '{"model":"gpt-5","input":"Get the weather and repository information","tools":[{"type":"mcp","server_label":"weather","server_url":"https://demo-day.mcp.cloudflare.com/sse"},{"type":"mcp","server_label":"github_copilot","server_url":"https://api.githubcopilot.com/mcp/","authorization":"GITHUB_MCP_TOKEN"}]}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json + User-Agent: + - OpenAI::Client/Ruby 0.35.1 + Host: + - api.openai.com + X-Stainless-Arch: + - arm64 + X-Stainless-Lang: + - ruby + X-Stainless-Os: + - MacOS + X-Stainless-Package-Version: + - 0.35.1 + X-Stainless-Runtime: + - ruby + X-Stainless-Runtime-Version: + - 3.4.7 + Content-Type: + - application/json + Openai-Organization: + - ORGANIZATION_ID + Openai-Project: + - PROJECT_ID + Authorization: + - Bearer ACCESS_TOKEN + X-Stainless-Retry-Count: + - '0' + X-Stainless-Timeout: + - '600.0' + Content-Length: + - '384' + response: + status: + code: 200 + message: OK + headers: + Date: + - Wed, 19 Nov 2025 20:47:01 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + X-Ratelimit-Limit-Requests: + - '15000' + X-Ratelimit-Limit-Tokens: + - '40000000' + X-Ratelimit-Remaining-Requests: + - '14999' + X-Ratelimit-Remaining-Tokens: + - '39999742' + X-Ratelimit-Reset-Requests: + - 4ms + X-Ratelimit-Reset-Tokens: + - 0s + Openai-Version: + - '2020-10-01' + Openai-Organization: + - ORGANIZATION_ID + Openai-Project: + - PROJECT_ID + X-Request-Id: + - req_3a6c3c814288492cb4c9bcffda99d81e + Openai-Processing-Ms: + - '12146' + X-Envoy-Upstream-Service-Time: + - '12150' + Cf-Cache-Status: + - DYNAMIC + Set-Cookie: + - __cf_bm=j2.cfQDVPxlsLwWrqPmnoLgRx6FjOvdhDAdZeNoiwws-1763585221-1.0.1.1-xMHbVgX_aU0ZBtQgMiIqXH.XN1a9SRt24.IvJaTK_G54jLIwVPqeBROke.LVRN2VAjY0eGnsP9daFP_4kSIvr6jIFCR0jOKkDvFXsM7Usgo; + path=/; expires=Wed, 19-Nov-25 21:17:01 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=HfhDIJwloIhF0Ex4_rIqS8SBlLrJguf2kwC9GiP7aHE-1763585221220-0.0.1.1-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + X-Content-Type-Options: + - nosniff + Server: + - cloudflare + Cf-Ray: + - 9a128f247e6715e0-SJC + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: |- + { + "id": "resp_01142e73b6db117e00691e2cb911f88198b67e43d7dc3c2291", + "object": "response", + "created_at": 1763585209, + "status": "completed", + "background": false, + "billing": { + "payer": "developer" + }, + "error": null, + "incomplete_details": null, + "instructions": null, + "max_output_tokens": null, + "max_tool_calls": null, + "model": "gpt-5-2025-08-07", + "output": [ + { + "id": "mcpl_01142e73b6db117e00691e2cb968608198a41622b3b25afc00", + "type": "mcp_list_tools", + "server_label": "weather", + "tools": [ + { + "annotations": { + "read_only": false + }, + "description": "Get information about Cloudflare's MCP Demo Day. Use this tool if the user asks about Cloudflare's MCP demo day", + "input_schema": { + "type": "object", + "properties": {} + }, + "name": "mcp_demo_day_info" + } + ] + }, + { + "id": "mcpl_01142e73b6db117e00691e2cb968f081989cfd717a72971239", + "type": "mcp_list_tools", + "server_label": "github_copilot", + "tools": [ + { + "annotations": { + "read_only": false + }, + "description": "Add review comment to the requester's latest pending pull request review. A pending review needs to already exist to call this (check with the user if not sure).", + "input_schema": { + "properties": { + "body": { + "description": "The text of the review comment", + "type": "string" + }, + "line": { + "description": "The line of the blob in the pull request diff that the comment applies to. For multi-line comments, the last line of the range", + "type": "number" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "path": { + "description": "The relative path to the file that necessitates a comment", + "type": "string" + }, + "pullNumber": { + "description": "Pull request number", + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + }, + "side": { + "description": "The side of the diff to comment on. LEFT indicates the previous state, RIGHT indicates the new state", + "enum": [ + "LEFT", + "RIGHT" + ], + "type": "string" + }, + "startLine": { + "description": "For multi-line comments, the first line of the range that the comment applies to", + "type": "number" + }, + "startSide": { + "description": "For multi-line comments, the starting side of the diff that the comment applies to. LEFT indicates the previous state, RIGHT indicates the new state", + "enum": [ + "LEFT", + "RIGHT" + ], + "type": "string" + }, + "subjectType": { + "description": "The level at which the comment is targeted", + "enum": [ + "FILE", + "LINE" + ], + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "pullNumber", + "path", + "body", + "subjectType" + ], + "type": "object" + }, + "name": "add_comment_to_pending_review" + }, + { + "annotations": { + "read_only": false + }, + "description": "Add a comment to a specific issue in a GitHub repository. Use this tool to add comments to pull requests as well (in this case pass pull request number as issue_number), but only if user is not asking specifically to add review comments.", + "input_schema": { + "properties": { + "body": { + "description": "Comment content", + "type": "string" + }, + "issue_number": { + "description": "Issue number to comment on", + "type": "number" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "issue_number", + "body" + ], + "type": "object" + }, + "name": "add_issue_comment" + }, + { + "annotations": { + "read_only": false + }, + "description": "Assign Copilot to a specific issue in a GitHub repository.\n\nThis tool can help with the following outcomes:\n- a Pull Request created with source code changes to resolve the issue\n\n\nMore information can be found at:\n- https://docs.github.com/en/copilot/using-github-copilot/using-copilot-coding-agent-to-work-on-tasks/about-assigning-tasks-to-copilot\n", + "input_schema": { + "properties": { + "issueNumber": { + "description": "Issue number", + "type": "number" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "issueNumber" + ], + "type": "object" + }, + "name": "assign_copilot_to_issue" + }, + { + "annotations": { + "read_only": false + }, + "description": "Create a new branch in a GitHub repository", + "input_schema": { + "properties": { + "branch": { + "description": "Name for new branch", + "type": "string" + }, + "from_branch": { + "description": "Source branch (defaults to repo default)", + "type": "string" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "branch" + ], + "type": "object" + }, + "name": "create_branch" + }, + { + "annotations": { + "read_only": false + }, + "description": "Create or update a single file in a GitHub repository. If updating, you must provide the SHA of the file you want to update. Use this tool to create or update a file in a GitHub repository remotely; do not use it for local file operations.", + "input_schema": { + "properties": { + "branch": { + "description": "Branch to create/update the file in", + "type": "string" + }, + "content": { + "description": "Content of the file", + "type": "string" + }, + "message": { + "description": "Commit message", + "type": "string" + }, + "owner": { + "description": "Repository owner (username or organization)", + "type": "string" + }, + "path": { + "description": "Path where to create/update the file", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + }, + "sha": { + "description": "Required if updating an existing file. The blob SHA of the file being replaced.", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "path", + "content", + "message", + "branch" + ], + "type": "object" + }, + "name": "create_or_update_file" + }, + { + "annotations": { + "read_only": false + }, + "description": "Create a new pull request in a GitHub repository.", + "input_schema": { + "properties": { + "base": { + "description": "Branch to merge into", + "type": "string" + }, + "body": { + "description": "PR description", + "type": "string" + }, + "draft": { + "description": "Create as draft PR", + "type": "boolean" + }, + "head": { + "description": "Branch containing changes", + "type": "string" + }, + "maintainer_can_modify": { + "description": "Allow maintainer edits", + "type": "boolean" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + }, + "title": { + "description": "PR title", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "title", + "head", + "base" + ], + "type": "object" + }, + "name": "create_pull_request" + }, + { + "annotations": { + "read_only": false + }, + "description": "Create a new GitHub repository in your account or specified organization", + "input_schema": { + "properties": { + "autoInit": { + "description": "Initialize with README", + "type": "boolean" + }, + "description": { + "description": "Repository description", + "type": "string" + }, + "name": { + "description": "Repository name", + "type": "string" + }, + "organization": { + "description": "Organization to create the repository in (omit to create in your personal account)", + "type": "string" + }, + "private": { + "description": "Whether repo should be private", + "type": "boolean" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "name": "create_repository" + }, + { + "annotations": { + "read_only": false + }, + "description": "Delete a file from a GitHub repository", + "input_schema": { + "properties": { + "branch": { + "description": "Branch to delete the file from", + "type": "string" + }, + "message": { + "description": "Commit message", + "type": "string" + }, + "owner": { + "description": "Repository owner (username or organization)", + "type": "string" + }, + "path": { + "description": "Path to the file to delete", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "path", + "message", + "branch" + ], + "type": "object" + }, + "name": "delete_file" + }, + { + "annotations": { + "read_only": false + }, + "description": "Fork a GitHub repository to your account or specified organization", + "input_schema": { + "properties": { + "organization": { + "description": "Organization to fork to", + "type": "string" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo" + ], + "type": "object" + }, + "name": "fork_repository" + }, + { + "annotations": { + "read_only": true + }, + "description": "Get details for a commit from a GitHub repository", + "input_schema": { + "properties": { + "include_diff": { + "default": true, + "description": "Whether to include file diffs and stats in the response. Default is true.", + "type": "boolean" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "page": { + "description": "Page number for pagination (min 1)", + "minimum": 1, + "type": "number" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + }, + "sha": { + "description": "Commit SHA, branch name, or tag name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "sha" + ], + "type": "object" + }, + "name": "get_commit" + }, + { + "annotations": { + "read_only": true + }, + "description": "Get the contents of a file or directory from a GitHub repository", + "input_schema": { + "properties": { + "owner": { + "description": "Repository owner (username or organization)", + "type": "string" + }, + "path": { + "default": "/", + "description": "Path to file/directory (directories must end with a slash '/')", + "type": "string" + }, + "ref": { + "description": "Accepts optional git refs such as `refs/tags/{tag}`, `refs/heads/{branch}` or `refs/pull/{pr_number}/head`", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + }, + "sha": { + "description": "Accepts optional commit SHA. If specified, it will be used instead of ref", + "type": "string" + } + }, + "required": [ + "owner", + "repo" + ], + "type": "object" + }, + "name": "get_file_contents" + }, + { + "annotations": { + "read_only": true + }, + "description": "Get a specific label from a repository.", + "input_schema": { + "properties": { + "name": { + "description": "Label name.", + "type": "string" + }, + "owner": { + "description": "Repository owner (username or organization name)", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "name" + ], + "type": "object" + }, + "name": "get_label" + }, + { + "annotations": { + "read_only": true + }, + "description": "Get the latest release in a GitHub repository", + "input_schema": { + "properties": { + "owner": { + "description": "Repository owner", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo" + ], + "type": "object" + }, + "name": "get_latest_release" + }, + { + "annotations": { + "read_only": true + }, + "description": "Get details of the authenticated GitHub user. Use this when a request is about the user's own profile for GitHub. Or when information is missing to build other tool calls.", + "input_schema": { + "properties": {}, + "type": "object" + }, + "name": "get_me" + }, + { + "annotations": { + "read_only": true + }, + "description": "Get a specific release by its tag name in a GitHub repository", + "input_schema": { + "properties": { + "owner": { + "description": "Repository owner", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + }, + "tag": { + "description": "Tag name (e.g., 'v1.0.0')", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "tag" + ], + "type": "object" + }, + "name": "get_release_by_tag" + }, + { + "annotations": { + "read_only": true + }, + "description": "Get details about a specific git tag in a GitHub repository", + "input_schema": { + "properties": { + "owner": { + "description": "Repository owner", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + }, + "tag": { + "description": "Tag name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "tag" + ], + "type": "object" + }, + "name": "get_tag" + }, + { + "annotations": { + "read_only": true + }, + "description": "Get member usernames of a specific team in an organization. Limited to organizations accessible with current credentials", + "input_schema": { + "properties": { + "org": { + "description": "Organization login (owner) that contains the team.", + "type": "string" + }, + "team_slug": { + "description": "Team slug", + "type": "string" + } + }, + "required": [ + "org", + "team_slug" + ], + "type": "object" + }, + "name": "get_team_members" + }, + { + "annotations": { + "read_only": true + }, + "description": "Get details of the teams the user is a member of. Limited to organizations accessible with current credentials", + "input_schema": { + "properties": { + "user": { + "description": "Username to get teams for. If not provided, uses the authenticated user.", + "type": "string" + } + }, + "type": "object" + }, + "name": "get_teams" + }, + { + "annotations": { + "read_only": true + }, + "description": "Get information about a specific issue in a GitHub repository.", + "input_schema": { + "properties": { + "issue_number": { + "description": "The number of the issue", + "type": "number" + }, + "method": { + "description": "The read operation to perform on a single issue. \nOptions are: \n1. get - Get details of a specific issue.\n2. get_comments - Get issue comments.\n3. get_sub_issues - Get sub-issues of the issue.\n4. get_labels - Get labels assigned to the issue.\n", + "enum": [ + "get", + "get_comments", + "get_sub_issues", + "get_labels" + ], + "type": "string" + }, + "owner": { + "description": "The owner of the repository", + "type": "string" + }, + "page": { + "description": "Page number for pagination (min 1)", + "minimum": 1, + "type": "number" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "repo": { + "description": "The name of the repository", + "type": "string" + } + }, + "required": [ + "method", + "owner", + "repo", + "issue_number" + ], + "type": "object" + }, + "name": "issue_read" + }, + { + "annotations": { + "read_only": false + }, + "description": "Create a new or update an existing issue in a GitHub repository.", + "input_schema": { + "properties": { + "assignees": { + "description": "Usernames to assign to this issue", + "items": { + "type": "string" + }, + "type": "array" + }, + "body": { + "description": "Issue body content", + "type": "string" + }, + "duplicate_of": { + "description": "Issue number that this issue is a duplicate of. Only used when state_reason is 'duplicate'.", + "type": "number" + }, + "issue_number": { + "description": "Issue number to update", + "type": "number" + }, + "labels": { + "description": "Labels to apply to this issue", + "items": { + "type": "string" + }, + "type": "array" + }, + "method": { + "description": "Write operation to perform on a single issue.\nOptions are: \n- 'create' - creates a new issue. \n- 'update' - updates an existing issue.\n", + "enum": [ + "create", + "update" + ], + "type": "string" + }, + "milestone": { + "description": "Milestone number", + "type": "number" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + }, + "state": { + "description": "New state", + "enum": [ + "open", + "closed" + ], + "type": "string" + }, + "state_reason": { + "description": "Reason for the state change. Ignored unless state is changed.", + "enum": [ + "completed", + "not_planned", + "duplicate" + ], + "type": "string" + }, + "title": { + "description": "Issue title", + "type": "string" + }, + "type": { + "description": "Type of this issue. Only use if the repository has issue types configured. Use list_issue_types tool to get valid type values for the organization. If the repository doesn't support issue types, omit this parameter.", + "type": "string" + } + }, + "required": [ + "method", + "owner", + "repo" + ], + "type": "object" + }, + "name": "issue_write" + }, + { + "annotations": { + "read_only": true + }, + "description": "List branches in a GitHub repository", + "input_schema": { + "properties": { + "owner": { + "description": "Repository owner", + "type": "string" + }, + "page": { + "description": "Page number for pagination (min 1)", + "minimum": 1, + "type": "number" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo" + ], + "type": "object" + }, + "name": "list_branches" + }, + { + "annotations": { + "read_only": true + }, + "description": "Get list of commits of a branch in a GitHub repository. Returns at least 30 results per page by default, but can return more if specified using the perPage parameter (up to 100).", + "input_schema": { + "properties": { + "author": { + "description": "Author username or email address to filter commits by", + "type": "string" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "page": { + "description": "Page number for pagination (min 1)", + "minimum": 1, + "type": "number" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + }, + "sha": { + "description": "Commit SHA, branch or tag name to list commits of. If not provided, uses the default branch of the repository. If a commit SHA is provided, will list commits up to that SHA.", + "type": "string" + } + }, + "required": [ + "owner", + "repo" + ], + "type": "object" + }, + "name": "list_commits" + }, + { + "annotations": { + "read_only": true + }, + "description": "List supported issue types for repository owner (organization).", + "input_schema": { + "properties": { + "owner": { + "description": "The organization owner of the repository", + "type": "string" + } + }, + "required": [ + "owner" + ], + "type": "object" + }, + "name": "list_issue_types" + }, + { + "annotations": { + "read_only": true + }, + "description": "List issues in a GitHub repository. For pagination, use the 'endCursor' from the previous response's 'pageInfo' in the 'after' parameter.", + "input_schema": { + "properties": { + "after": { + "description": "Cursor for pagination. Use the endCursor from the previous page's PageInfo for GraphQL APIs.", + "type": "string" + }, + "direction": { + "description": "Order direction. If provided, the 'orderBy' also needs to be provided.", + "enum": [ + "ASC", + "DESC" + ], + "type": "string" + }, + "labels": { + "description": "Filter by labels", + "items": { + "type": "string" + }, + "type": "array" + }, + "orderBy": { + "description": "Order issues by field. If provided, the 'direction' also needs to be provided.", + "enum": [ + "CREATED_AT", + "UPDATED_AT", + "COMMENTS" + ], + "type": "string" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + }, + "since": { + "description": "Filter by date (ISO 8601 timestamp)", + "type": "string" + }, + "state": { + "description": "Filter by state, by default both open and closed issues are returned when not provided", + "enum": [ + "OPEN", + "CLOSED" + ], + "type": "string" + } + }, + "required": [ + "owner", + "repo" + ], + "type": "object" + }, + "name": "list_issues" + }, + { + "annotations": { + "read_only": true + }, + "description": "List pull requests in a GitHub repository. If the user specifies an author, then DO NOT use this tool and use the search_pull_requests tool instead.", + "input_schema": { + "properties": { + "base": { + "description": "Filter by base branch", + "type": "string" + }, + "direction": { + "description": "Sort direction", + "enum": [ + "asc", + "desc" + ], + "type": "string" + }, + "head": { + "description": "Filter by head user/org and branch", + "type": "string" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "page": { + "description": "Page number for pagination (min 1)", + "minimum": 1, + "type": "number" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + }, + "sort": { + "description": "Sort by", + "enum": [ + "created", + "updated", + "popularity", + "long-running" + ], + "type": "string" + }, + "state": { + "description": "Filter by state", + "enum": [ + "open", + "closed", + "all" + ], + "type": "string" + } + }, + "required": [ + "owner", + "repo" + ], + "type": "object" + }, + "name": "list_pull_requests" + }, + { + "annotations": { + "read_only": true + }, + "description": "List releases in a GitHub repository", + "input_schema": { + "properties": { + "owner": { + "description": "Repository owner", + "type": "string" + }, + "page": { + "description": "Page number for pagination (min 1)", + "minimum": 1, + "type": "number" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo" + ], + "type": "object" + }, + "name": "list_releases" + }, + { + "annotations": { + "read_only": true + }, + "description": "List git tags in a GitHub repository", + "input_schema": { + "properties": { + "owner": { + "description": "Repository owner", + "type": "string" + }, + "page": { + "description": "Page number for pagination (min 1)", + "minimum": 1, + "type": "number" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo" + ], + "type": "object" + }, + "name": "list_tags" + }, + { + "annotations": { + "read_only": false + }, + "description": "Merge a pull request in a GitHub repository.", + "input_schema": { + "properties": { + "commit_message": { + "description": "Extra detail for merge commit", + "type": "string" + }, + "commit_title": { + "description": "Title for merge commit", + "type": "string" + }, + "merge_method": { + "description": "Merge method", + "enum": [ + "merge", + "squash", + "rebase" + ], + "type": "string" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "pullNumber": { + "description": "Pull request number", + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "pullNumber" + ], + "type": "object" + }, + "name": "merge_pull_request" + }, + { + "annotations": { + "read_only": true + }, + "description": "Get information on a specific pull request in GitHub repository.", + "input_schema": { + "properties": { + "method": { + "description": "Action to specify what pull request data needs to be retrieved from GitHub. \nPossible options: \n 1. get - Get details of a specific pull request.\n 2. get_diff - Get the diff of a pull request.\n 3. get_status - Get status of a head commit in a pull request. This reflects status of builds and checks.\n 4. get_files - Get the list of files changed in a pull request. Use with pagination parameters to control the number of results returned.\n 5. get_review_comments - Get the review comments on a pull request. They are comments made on a portion of the unified diff during a pull request review. Use with pagination parameters to control the number of results returned.\n 6. get_reviews - Get the reviews on a pull request. When asked for review comments, use get_review_comments method.\n 7. get_comments - Get comments on a pull request. Use this if user doesn't specifically want review comments. Use with pagination parameters to control the number of results returned.\n", + "enum": [ + "get", + "get_diff", + "get_status", + "get_files", + "get_review_comments", + "get_reviews", + "get_comments" + ], + "type": "string" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "page": { + "description": "Page number for pagination (min 1)", + "minimum": 1, + "type": "number" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "pullNumber": { + "description": "Pull request number", + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "method", + "owner", + "repo", + "pullNumber" + ], + "type": "object" + }, + "name": "pull_request_read" + }, + { + "annotations": { + "read_only": false + }, + "description": "Create and/or submit, delete review of a pull request.\n\nAvailable methods:\n- create: Create a new review of a pull request. If \"event\" parameter is provided, the review is submitted. If \"event\" is omitted, a pending review is created.\n- submit_pending: Submit an existing pending review of a pull request. This requires that a pending review exists for the current user on the specified pull request. The \"body\" and \"event\" parameters are used when submitting the review.\n- delete_pending: Delete an existing pending review of a pull request. This requires that a pending review exists for the current user on the specified pull request.\n", + "input_schema": { + "properties": { + "body": { + "description": "Review comment text", + "type": "string" + }, + "commitID": { + "description": "SHA of commit to review", + "type": "string" + }, + "event": { + "description": "Review action to perform.", + "enum": [ + "APPROVE", + "REQUEST_CHANGES", + "COMMENT" + ], + "type": "string" + }, + "method": { + "description": "The write operation to perform on pull request review.", + "enum": [ + "create", + "submit_pending", + "delete_pending" + ], + "type": "string" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "pullNumber": { + "description": "Pull request number", + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "method", + "owner", + "repo", + "pullNumber" + ], + "type": "object" + }, + "name": "pull_request_review_write" + }, + { + "annotations": { + "read_only": false + }, + "description": "Push multiple files to a GitHub repository in a single commit", + "input_schema": { + "properties": { + "branch": { + "description": "Branch to push to", + "type": "string" + }, + "files": { + "description": "Array of file objects to push, each object with path (string) and content (string)", + "items": { + "additionalProperties": false, + "properties": { + "content": { + "description": "file content", + "type": "string" + }, + "path": { + "description": "path to the file", + "type": "string" + } + }, + "required": [ + "path", + "content" + ], + "type": "object" + }, + "type": "array" + }, + "message": { + "description": "Commit message", + "type": "string" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "branch", + "files", + "message" + ], + "type": "object" + }, + "name": "push_files" + }, + { + "annotations": { + "read_only": false + }, + "description": "Request a GitHub Copilot code review for a pull request. Use this for automated feedback on pull requests, usually before requesting a human reviewer.", + "input_schema": { + "properties": { + "owner": { + "description": "Repository owner", + "type": "string" + }, + "pullNumber": { + "description": "Pull request number", + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "pullNumber" + ], + "type": "object" + }, + "name": "request_copilot_review" + }, + { + "annotations": { + "read_only": true + }, + "description": "Fast and precise code search across ALL GitHub repositories using GitHub's native search engine. Best for finding exact symbols, functions, classes, or specific code patterns.", + "input_schema": { + "properties": { + "order": { + "description": "Sort order for results", + "enum": [ + "asc", + "desc" + ], + "type": "string" + }, + "page": { + "description": "Page number for pagination (min 1)", + "minimum": 1, + "type": "number" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "query": { + "description": "Search query using GitHub's powerful code search syntax. Examples: 'content:Skill language:Java org:github', 'NOT is:archived language:Python OR language:go', 'repo:github/github-mcp-server'. Supports exact matching, language filters, path filters, and more.", + "type": "string" + }, + "sort": { + "description": "Sort field ('indexed' only)", + "type": "string" + } + }, + "required": [ + "query" + ], + "type": "object" + }, + "name": "search_code" + }, + { + "annotations": { + "read_only": true + }, + "description": "Search for issues in GitHub repositories using issues search syntax already scoped to is:issue", + "input_schema": { + "properties": { + "order": { + "description": "Sort order", + "enum": [ + "asc", + "desc" + ], + "type": "string" + }, + "owner": { + "description": "Optional repository owner. If provided with repo, only issues for this repository are listed.", + "type": "string" + }, + "page": { + "description": "Page number for pagination (min 1)", + "minimum": 1, + "type": "number" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "query": { + "description": "Search query using GitHub issues search syntax", + "type": "string" + }, + "repo": { + "description": "Optional repository name. If provided with owner, only issues for this repository are listed.", + "type": "string" + }, + "sort": { + "description": "Sort field by number of matches of categories, defaults to best match", + "enum": [ + "comments", + "reactions", + "reactions-+1", + "reactions--1", + "reactions-smile", + "reactions-thinking_face", + "reactions-heart", + "reactions-tada", + "interactions", + "created", + "updated" + ], + "type": "string" + } + }, + "required": [ + "query" + ], + "type": "object" + }, + "name": "search_issues" + }, + { + "annotations": { + "read_only": true + }, + "description": "Search for pull requests in GitHub repositories using issues search syntax already scoped to is:pr", + "input_schema": { + "properties": { + "order": { + "description": "Sort order", + "enum": [ + "asc", + "desc" + ], + "type": "string" + }, + "owner": { + "description": "Optional repository owner. If provided with repo, only pull requests for this repository are listed.", + "type": "string" + }, + "page": { + "description": "Page number for pagination (min 1)", + "minimum": 1, + "type": "number" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "query": { + "description": "Search query using GitHub pull request search syntax", + "type": "string" + }, + "repo": { + "description": "Optional repository name. If provided with owner, only pull requests for this repository are listed.", + "type": "string" + }, + "sort": { + "description": "Sort field by number of matches of categories, defaults to best match", + "enum": [ + "comments", + "reactions", + "reactions-+1", + "reactions--1", + "reactions-smile", + "reactions-thinking_face", + "reactions-heart", + "reactions-tada", + "interactions", + "created", + "updated" + ], + "type": "string" + } + }, + "required": [ + "query" + ], + "type": "object" + }, + "name": "search_pull_requests" + }, + { + "annotations": { + "read_only": true + }, + "description": "Find GitHub repositories by name, description, readme, topics, or other metadata. Perfect for discovering projects, finding examples, or locating specific repositories across GitHub.", + "input_schema": { + "properties": { + "minimal_output": { + "default": true, + "description": "Return minimal repository information (default: true). When false, returns full GitHub API repository objects.", + "type": "boolean" + }, + "order": { + "description": "Sort order", + "enum": [ + "asc", + "desc" + ], + "type": "string" + }, + "page": { + "description": "Page number for pagination (min 1)", + "minimum": 1, + "type": "number" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "query": { + "description": "Repository search query. Examples: 'machine learning in:name stars:>1000 language:python', 'topic:react', 'user:facebook'. Supports advanced search syntax for precise filtering.", + "type": "string" + }, + "sort": { + "description": "Sort repositories by field, defaults to best match", + "enum": [ + "stars", + "forks", + "help-wanted-issues", + "updated" + ], + "type": "string" + } + }, + "required": [ + "query" + ], + "type": "object" + }, + "name": "search_repositories" + }, + { + "annotations": { + "read_only": true + }, + "description": "Find GitHub users by username, real name, or other profile information. Useful for locating developers, contributors, or team members.", + "input_schema": { + "properties": { + "order": { + "description": "Sort order", + "enum": [ + "asc", + "desc" + ], + "type": "string" + }, + "page": { + "description": "Page number for pagination (min 1)", + "minimum": 1, + "type": "number" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "query": { + "description": "User search query. Examples: 'john smith', 'location:seattle', 'followers:>100'. Search is automatically scoped to type:user.", + "type": "string" + }, + "sort": { + "description": "Sort users by number of followers or repositories, or when the person joined GitHub.", + "enum": [ + "followers", + "repositories", + "joined" + ], + "type": "string" + } + }, + "required": [ + "query" + ], + "type": "object" + }, + "name": "search_users" + }, + { + "annotations": { + "read_only": false + }, + "description": "Add a sub-issue to a parent issue in a GitHub repository.", + "input_schema": { + "properties": { + "after_id": { + "description": "The ID of the sub-issue to be prioritized after (either after_id OR before_id should be specified)", + "type": "number" + }, + "before_id": { + "description": "The ID of the sub-issue to be prioritized before (either after_id OR before_id should be specified)", + "type": "number" + }, + "issue_number": { + "description": "The number of the parent issue", + "type": "number" + }, + "method": { + "description": "The action to perform on a single sub-issue\nOptions are:\n- 'add' - add a sub-issue to a parent issue in a GitHub repository.\n- 'remove' - remove a sub-issue from a parent issue in a GitHub repository.\n- 'reprioritize' - change the order of sub-issues within a parent issue in a GitHub repository. Use either 'after_id' or 'before_id' to specify the new position.\n\t\t\t\t", + "type": "string" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "replace_parent": { + "description": "When true, replaces the sub-issue's current parent issue. Use with 'add' method only.", + "type": "boolean" + }, + "repo": { + "description": "Repository name", + "type": "string" + }, + "sub_issue_id": { + "description": "The ID of the sub-issue to add. ID is not the same as issue number", + "type": "number" + } + }, + "required": [ + "method", + "owner", + "repo", + "issue_number", + "sub_issue_id" + ], + "type": "object" + }, + "name": "sub_issue_write" + }, + { + "annotations": { + "read_only": false + }, + "description": "Update an existing pull request in a GitHub repository.", + "input_schema": { + "properties": { + "base": { + "description": "New base branch name", + "type": "string" + }, + "body": { + "description": "New description", + "type": "string" + }, + "draft": { + "description": "Mark pull request as draft (true) or ready for review (false)", + "type": "boolean" + }, + "maintainer_can_modify": { + "description": "Allow maintainer edits", + "type": "boolean" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "pullNumber": { + "description": "Pull request number to update", + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + }, + "reviewers": { + "description": "GitHub usernames to request reviews from", + "items": { + "type": "string" + }, + "type": "array" + }, + "state": { + "description": "New state", + "enum": [ + "open", + "closed" + ], + "type": "string" + }, + "title": { + "description": "New title", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "pullNumber" + ], + "type": "object" + }, + "name": "update_pull_request" + }, + { + "annotations": { + "read_only": false + }, + "description": "Update the branch of a pull request with the latest changes from the base branch.", + "input_schema": { + "properties": { + "expectedHeadSha": { + "description": "The expected SHA of the pull request's HEAD ref", + "type": "string" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "pullNumber": { + "description": "Pull request number", + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "pullNumber" + ], + "type": "object" + }, + "name": "update_pull_request_branch" + } + ] + }, + { + "id": "rs_01142e73b6db117e00691e2cbb47308198b2c8f0e0c04ed9e7", + "type": "reasoning", + "summary": [] + }, + { + "id": "msg_01142e73b6db117e00691e2cc3337c8198a4606717b4ac6447", + "type": "message", + "status": "completed", + "content": [ + { + "type": "output_text", + "annotations": [], + "logprobs": [], + "text": "Happy to help! To get started, I need a couple details:\n\n- Weather:\n - Which location (city and country or ZIP/postcode)?\n - Do you want current conditions, today\u2019s forecast, or a multi\u2011day forecast?\n\n- Repository information:\n - Which GitHub repository (owner/name or a URL)?\n - What details do you want (e.g., branches, latest commits, open issues/PRs, releases, tags)?" + } + ], + "role": "assistant" + } + ], + "parallel_tool_calls": true, + "previous_response_id": null, + "prompt_cache_key": null, + "prompt_cache_retention": null, + "reasoning": { + "effort": "medium", + "summary": null + }, + "safety_identifier": null, + "service_tier": "default", + "store": true, + "temperature": 1.0, + "text": { + "format": { + "type": "text" + }, + "verbosity": "medium" + }, + "tool_choice": "auto", + "tools": [ + { + "type": "mcp", + "allowed_tools": null, + "headers": null, + "require_approval": "always", + "server_description": null, + "server_label": "weather", + "server_url": "https://demo-day.mcp.cloudflare.com/sse" + }, + { + "type": "mcp", + "allowed_tools": null, + "authorization": "", + "headers": null, + "require_approval": "always", + "server_description": null, + "server_label": "github_copilot", + "server_url": "https://api.githubcopilot.com/mcp/" + } + ], + "top_logprobs": 0, + "top_p": 1.0, + "truncation": "disabled", + "usage": { + "input_tokens": 5369, + "input_tokens_details": { + "cached_tokens": 5248 + }, + "output_tokens": 482, + "output_tokens_details": { + "reasoning_tokens": 384 + }, + "total_tokens": 5851 + }, + "user": null, + "metadata": {} + } + recorded_at: Wed, 19 Nov 2025 20:47:01 GMT +recorded_with: VCR 6.3.1 diff --git a/test/fixtures/vcr_cassettes/integration/open_ai/responses/common_format/mcp_test/test_agent_common_format_single_server.yml b/test/fixtures/vcr_cassettes/integration/open_ai/responses/common_format/mcp_test/test_agent_common_format_single_server.yml new file mode 100644 index 00000000..c4e4aa8f --- /dev/null +++ b/test/fixtures/vcr_cassettes/integration/open_ai/responses/common_format/mcp_test/test_agent_common_format_single_server.yml @@ -0,0 +1,204 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.openai.com/v1/responses + body: + encoding: UTF-8 + string: '{"model":"gpt-5","input":"Get the current weather","tools":[{"type":"mcp","server_label":"weather","server_url":"https://demo-day.mcp.cloudflare.com/sse"}]}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json + User-Agent: + - OpenAI::Client/Ruby 0.35.1 + Host: + - api.openai.com + X-Stainless-Arch: + - arm64 + X-Stainless-Lang: + - ruby + X-Stainless-Os: + - MacOS + X-Stainless-Package-Version: + - 0.35.1 + X-Stainless-Runtime: + - ruby + X-Stainless-Runtime-Version: + - 3.4.7 + Content-Type: + - application/json + Openai-Organization: + - ORGANIZATION_ID + Openai-Project: + - PROJECT_ID + Authorization: + - Bearer ACCESS_TOKEN + X-Stainless-Retry-Count: + - '0' + X-Stainless-Timeout: + - '600.0' + Content-Length: + - '156' + response: + status: + code: 200 + message: OK + headers: + Date: + - Wed, 19 Nov 2025 20:46:21 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + X-Ratelimit-Limit-Requests: + - '15000' + X-Ratelimit-Limit-Tokens: + - '40000000' + X-Ratelimit-Remaining-Requests: + - '14999' + X-Ratelimit-Remaining-Tokens: + - '39999994' + X-Ratelimit-Reset-Requests: + - 4ms + X-Ratelimit-Reset-Tokens: + - 0s + Openai-Version: + - '2020-10-01' + Openai-Organization: + - ORGANIZATION_ID + Openai-Project: + - PROJECT_ID + X-Request-Id: + - req_a0c5f02486ed497abb5d00386934bdd8 + Openai-Processing-Ms: + - '8667' + X-Envoy-Upstream-Service-Time: + - '8670' + Cf-Cache-Status: + - DYNAMIC + Set-Cookie: + - __cf_bm=hEMCBTQW2kbtAW3WZlboz0.6poiAhEfPOmz0KuCKxBA-1763585181-1.0.1.1-6CSuuuouS3h6lNKBeSKHyL02OMvCAYjGjtPdJVJAAIVrsQ8HAk2vSUyOuXNN17MJ1eEXsBSQFV4QuH._fMIpsYxFwn.DZjjH9MjdBYkZb8o; + path=/; expires=Wed, 19-Nov-25 21:16:21 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=kWh83s9g4apTauc3btcfmRN7gISFSYbxm6AUx7mq0_c-1763585181322-0.0.1.1-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + X-Content-Type-Options: + - nosniff + Server: + - cloudflare + Cf-Ray: + - 9a128e40ad027aaf-SJC + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: |- + { + "id": "resp_0a61b801b444d08000691e2c94a29c8197912046d330286604", + "object": "response", + "created_at": 1763585172, + "status": "completed", + "background": false, + "billing": { + "payer": "developer" + }, + "error": null, + "incomplete_details": null, + "instructions": null, + "max_output_tokens": null, + "max_tool_calls": null, + "model": "gpt-5-2025-08-07", + "output": [ + { + "id": "mcpl_0a61b801b444d08000691e2c94e7e48197b1c74f8cfdb06d07", + "type": "mcp_list_tools", + "server_label": "weather", + "tools": [ + { + "annotations": { + "read_only": false + }, + "description": "Get information about Cloudflare's MCP Demo Day. Use this tool if the user asks about Cloudflare's MCP demo day", + "input_schema": { + "type": "object", + "properties": {} + }, + "name": "mcp_demo_day_info" + } + ] + }, + { + "id": "rs_0a61b801b444d08000691e2c9698588197827178deb303ddb2", + "type": "reasoning", + "summary": [] + }, + { + "id": "msg_0a61b801b444d08000691e2c9c38688197ae98d8ca7b672d05", + "type": "message", + "status": "completed", + "content": [ + { + "type": "output_text", + "annotations": [], + "logprobs": [], + "text": "Sure\u2014what location should I use? Please share a city and country (or ZIP/postcode or coordinates), and your preferred units (C or F)." + } + ], + "role": "assistant" + } + ], + "parallel_tool_calls": true, + "previous_response_id": null, + "prompt_cache_key": null, + "prompt_cache_retention": null, + "reasoning": { + "effort": "medium", + "summary": null + }, + "safety_identifier": null, + "service_tier": "default", + "store": true, + "temperature": 1.0, + "text": { + "format": { + "type": "text" + }, + "verbosity": "medium" + }, + "tool_choice": "auto", + "tools": [ + { + "type": "mcp", + "allowed_tools": null, + "headers": null, + "require_approval": "always", + "server_description": null, + "server_label": "weather", + "server_url": "https://demo-day.mcp.cloudflare.com/sse" + } + ], + "top_logprobs": 0, + "top_p": 1.0, + "truncation": "disabled", + "usage": { + "input_tokens": 142, + "input_tokens_details": { + "cached_tokens": 0 + }, + "output_tokens": 229, + "output_tokens_details": { + "reasoning_tokens": 192 + }, + "total_tokens": 371 + }, + "user": null, + "metadata": {} + } + recorded_at: Wed, 19 Nov 2025 20:46:21 GMT +recorded_with: VCR 6.3.1 diff --git a/test/fixtures/vcr_cassettes/integration/open_ai/responses/common_format/mcp_test/test_agent_common_format_single_server_with_auth.yml b/test/fixtures/vcr_cassettes/integration/open_ai/responses/common_format/mcp_test/test_agent_common_format_single_server_with_auth.yml new file mode 100644 index 00000000..28b1a402 --- /dev/null +++ b/test/fixtures/vcr_cassettes/integration/open_ai/responses/common_format/mcp_test/test_agent_common_format_single_server_with_auth.yml @@ -0,0 +1,1911 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.openai.com/v1/responses + body: + encoding: UTF-8 + string: '{"model":"gpt-5","input":"Get repository information","tools":[{"type":"mcp","server_label":"github_copilot","server_url":"https://api.githubcopilot.com/mcp/","authorization":"GITHUB_MCP_TOKEN"}]}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json + User-Agent: + - OpenAI::Client/Ruby 0.35.1 + Host: + - api.openai.com + X-Stainless-Arch: + - arm64 + X-Stainless-Lang: + - ruby + X-Stainless-Os: + - MacOS + X-Stainless-Package-Version: + - 0.35.1 + X-Stainless-Runtime: + - ruby + X-Stainless-Runtime-Version: + - 3.4.7 + Content-Type: + - application/json + Openai-Organization: + - ORGANIZATION_ID + Openai-Project: + - PROJECT_ID + Authorization: + - Bearer ACCESS_TOKEN + X-Stainless-Retry-Count: + - '0' + X-Stainless-Timeout: + - '600.0' + Content-Length: + - '273' + response: + status: + code: 200 + message: OK + headers: + Date: + - Wed, 19 Nov 2025 20:46:12 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + X-Ratelimit-Limit-Requests: + - '15000' + X-Ratelimit-Limit-Tokens: + - '40000000' + X-Ratelimit-Remaining-Requests: + - '14999' + X-Ratelimit-Remaining-Tokens: + - '39999552' + X-Ratelimit-Reset-Requests: + - 4ms + X-Ratelimit-Reset-Tokens: + - 0s + Openai-Version: + - '2020-10-01' + Openai-Organization: + - ORGANIZATION_ID + Openai-Project: + - PROJECT_ID + X-Request-Id: + - req_725ba7e71fda420a94b554e9145875e4 + Openai-Processing-Ms: + - '18277' + X-Envoy-Upstream-Service-Time: + - '18280' + Cf-Cache-Status: + - DYNAMIC + Set-Cookie: + - __cf_bm=9Y5IzrkvtruiIpBJPjA_YtnMLViW09gnhXpCFU5MIr4-1763585172-1.0.1.1-2BtyiLqLt5Tk7jGO03TuGBSmxSEf9zkw7ePAewveycnA4zEyh1aeBbIl2repFN4fTRTSnVIx4O_4R9qCbIsypuWejk9VM9Z.hxB5QueNYsE; + path=/; expires=Wed, 19-Nov-25 21:16:12 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=XS4xf6HTx0IcH9mL6bPrTEpDK9hIAo3tN57JX3FLGBk-1763585172364-0.0.1.1-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + X-Content-Type-Options: + - nosniff + Server: + - cloudflare + Cf-Ray: + - 9a128dcc7dd0fa62-SJC + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: |- + { + "id": "resp_0bbb974cbbc378f500691e2c8216c0819891982946cb6cf0ad", + "object": "response", + "created_at": 1763585154, + "status": "completed", + "background": false, + "billing": { + "payer": "developer" + }, + "error": null, + "incomplete_details": null, + "instructions": null, + "max_output_tokens": null, + "max_tool_calls": null, + "model": "gpt-5-2025-08-07", + "output": [ + { + "id": "mcpl_0bbb974cbbc378f500691e2c82402c8198bdf001518e0d9a7e", + "type": "mcp_list_tools", + "server_label": "github_copilot", + "tools": [ + { + "annotations": { + "read_only": false + }, + "description": "Add review comment to the requester's latest pending pull request review. A pending review needs to already exist to call this (check with the user if not sure).", + "input_schema": { + "properties": { + "body": { + "description": "The text of the review comment", + "type": "string" + }, + "line": { + "description": "The line of the blob in the pull request diff that the comment applies to. For multi-line comments, the last line of the range", + "type": "number" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "path": { + "description": "The relative path to the file that necessitates a comment", + "type": "string" + }, + "pullNumber": { + "description": "Pull request number", + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + }, + "side": { + "description": "The side of the diff to comment on. LEFT indicates the previous state, RIGHT indicates the new state", + "enum": [ + "LEFT", + "RIGHT" + ], + "type": "string" + }, + "startLine": { + "description": "For multi-line comments, the first line of the range that the comment applies to", + "type": "number" + }, + "startSide": { + "description": "For multi-line comments, the starting side of the diff that the comment applies to. LEFT indicates the previous state, RIGHT indicates the new state", + "enum": [ + "LEFT", + "RIGHT" + ], + "type": "string" + }, + "subjectType": { + "description": "The level at which the comment is targeted", + "enum": [ + "FILE", + "LINE" + ], + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "pullNumber", + "path", + "body", + "subjectType" + ], + "type": "object" + }, + "name": "add_comment_to_pending_review" + }, + { + "annotations": { + "read_only": false + }, + "description": "Add a comment to a specific issue in a GitHub repository. Use this tool to add comments to pull requests as well (in this case pass pull request number as issue_number), but only if user is not asking specifically to add review comments.", + "input_schema": { + "properties": { + "body": { + "description": "Comment content", + "type": "string" + }, + "issue_number": { + "description": "Issue number to comment on", + "type": "number" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "issue_number", + "body" + ], + "type": "object" + }, + "name": "add_issue_comment" + }, + { + "annotations": { + "read_only": false + }, + "description": "Assign Copilot to a specific issue in a GitHub repository.\n\nThis tool can help with the following outcomes:\n- a Pull Request created with source code changes to resolve the issue\n\n\nMore information can be found at:\n- https://docs.github.com/en/copilot/using-github-copilot/using-copilot-coding-agent-to-work-on-tasks/about-assigning-tasks-to-copilot\n", + "input_schema": { + "properties": { + "issueNumber": { + "description": "Issue number", + "type": "number" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "issueNumber" + ], + "type": "object" + }, + "name": "assign_copilot_to_issue" + }, + { + "annotations": { + "read_only": false + }, + "description": "Create a new branch in a GitHub repository", + "input_schema": { + "properties": { + "branch": { + "description": "Name for new branch", + "type": "string" + }, + "from_branch": { + "description": "Source branch (defaults to repo default)", + "type": "string" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "branch" + ], + "type": "object" + }, + "name": "create_branch" + }, + { + "annotations": { + "read_only": false + }, + "description": "Create or update a single file in a GitHub repository. If updating, you must provide the SHA of the file you want to update. Use this tool to create or update a file in a GitHub repository remotely; do not use it for local file operations.", + "input_schema": { + "properties": { + "branch": { + "description": "Branch to create/update the file in", + "type": "string" + }, + "content": { + "description": "Content of the file", + "type": "string" + }, + "message": { + "description": "Commit message", + "type": "string" + }, + "owner": { + "description": "Repository owner (username or organization)", + "type": "string" + }, + "path": { + "description": "Path where to create/update the file", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + }, + "sha": { + "description": "Required if updating an existing file. The blob SHA of the file being replaced.", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "path", + "content", + "message", + "branch" + ], + "type": "object" + }, + "name": "create_or_update_file" + }, + { + "annotations": { + "read_only": false + }, + "description": "Create a new pull request in a GitHub repository.", + "input_schema": { + "properties": { + "base": { + "description": "Branch to merge into", + "type": "string" + }, + "body": { + "description": "PR description", + "type": "string" + }, + "draft": { + "description": "Create as draft PR", + "type": "boolean" + }, + "head": { + "description": "Branch containing changes", + "type": "string" + }, + "maintainer_can_modify": { + "description": "Allow maintainer edits", + "type": "boolean" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + }, + "title": { + "description": "PR title", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "title", + "head", + "base" + ], + "type": "object" + }, + "name": "create_pull_request" + }, + { + "annotations": { + "read_only": false + }, + "description": "Create a new GitHub repository in your account or specified organization", + "input_schema": { + "properties": { + "autoInit": { + "description": "Initialize with README", + "type": "boolean" + }, + "description": { + "description": "Repository description", + "type": "string" + }, + "name": { + "description": "Repository name", + "type": "string" + }, + "organization": { + "description": "Organization to create the repository in (omit to create in your personal account)", + "type": "string" + }, + "private": { + "description": "Whether repo should be private", + "type": "boolean" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "name": "create_repository" + }, + { + "annotations": { + "read_only": false + }, + "description": "Delete a file from a GitHub repository", + "input_schema": { + "properties": { + "branch": { + "description": "Branch to delete the file from", + "type": "string" + }, + "message": { + "description": "Commit message", + "type": "string" + }, + "owner": { + "description": "Repository owner (username or organization)", + "type": "string" + }, + "path": { + "description": "Path to the file to delete", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "path", + "message", + "branch" + ], + "type": "object" + }, + "name": "delete_file" + }, + { + "annotations": { + "read_only": false + }, + "description": "Fork a GitHub repository to your account or specified organization", + "input_schema": { + "properties": { + "organization": { + "description": "Organization to fork to", + "type": "string" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo" + ], + "type": "object" + }, + "name": "fork_repository" + }, + { + "annotations": { + "read_only": true + }, + "description": "Get details for a commit from a GitHub repository", + "input_schema": { + "properties": { + "include_diff": { + "default": true, + "description": "Whether to include file diffs and stats in the response. Default is true.", + "type": "boolean" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "page": { + "description": "Page number for pagination (min 1)", + "minimum": 1, + "type": "number" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + }, + "sha": { + "description": "Commit SHA, branch name, or tag name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "sha" + ], + "type": "object" + }, + "name": "get_commit" + }, + { + "annotations": { + "read_only": true + }, + "description": "Get the contents of a file or directory from a GitHub repository", + "input_schema": { + "properties": { + "owner": { + "description": "Repository owner (username or organization)", + "type": "string" + }, + "path": { + "default": "/", + "description": "Path to file/directory (directories must end with a slash '/')", + "type": "string" + }, + "ref": { + "description": "Accepts optional git refs such as `refs/tags/{tag}`, `refs/heads/{branch}` or `refs/pull/{pr_number}/head`", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + }, + "sha": { + "description": "Accepts optional commit SHA. If specified, it will be used instead of ref", + "type": "string" + } + }, + "required": [ + "owner", + "repo" + ], + "type": "object" + }, + "name": "get_file_contents" + }, + { + "annotations": { + "read_only": true + }, + "description": "Get a specific label from a repository.", + "input_schema": { + "properties": { + "name": { + "description": "Label name.", + "type": "string" + }, + "owner": { + "description": "Repository owner (username or organization name)", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "name" + ], + "type": "object" + }, + "name": "get_label" + }, + { + "annotations": { + "read_only": true + }, + "description": "Get the latest release in a GitHub repository", + "input_schema": { + "properties": { + "owner": { + "description": "Repository owner", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo" + ], + "type": "object" + }, + "name": "get_latest_release" + }, + { + "annotations": { + "read_only": true + }, + "description": "Get details of the authenticated GitHub user. Use this when a request is about the user's own profile for GitHub. Or when information is missing to build other tool calls.", + "input_schema": { + "properties": {}, + "type": "object" + }, + "name": "get_me" + }, + { + "annotations": { + "read_only": true + }, + "description": "Get a specific release by its tag name in a GitHub repository", + "input_schema": { + "properties": { + "owner": { + "description": "Repository owner", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + }, + "tag": { + "description": "Tag name (e.g., 'v1.0.0')", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "tag" + ], + "type": "object" + }, + "name": "get_release_by_tag" + }, + { + "annotations": { + "read_only": true + }, + "description": "Get details about a specific git tag in a GitHub repository", + "input_schema": { + "properties": { + "owner": { + "description": "Repository owner", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + }, + "tag": { + "description": "Tag name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "tag" + ], + "type": "object" + }, + "name": "get_tag" + }, + { + "annotations": { + "read_only": true + }, + "description": "Get member usernames of a specific team in an organization. Limited to organizations accessible with current credentials", + "input_schema": { + "properties": { + "org": { + "description": "Organization login (owner) that contains the team.", + "type": "string" + }, + "team_slug": { + "description": "Team slug", + "type": "string" + } + }, + "required": [ + "org", + "team_slug" + ], + "type": "object" + }, + "name": "get_team_members" + }, + { + "annotations": { + "read_only": true + }, + "description": "Get details of the teams the user is a member of. Limited to organizations accessible with current credentials", + "input_schema": { + "properties": { + "user": { + "description": "Username to get teams for. If not provided, uses the authenticated user.", + "type": "string" + } + }, + "type": "object" + }, + "name": "get_teams" + }, + { + "annotations": { + "read_only": true + }, + "description": "Get information about a specific issue in a GitHub repository.", + "input_schema": { + "properties": { + "issue_number": { + "description": "The number of the issue", + "type": "number" + }, + "method": { + "description": "The read operation to perform on a single issue. \nOptions are: \n1. get - Get details of a specific issue.\n2. get_comments - Get issue comments.\n3. get_sub_issues - Get sub-issues of the issue.\n4. get_labels - Get labels assigned to the issue.\n", + "enum": [ + "get", + "get_comments", + "get_sub_issues", + "get_labels" + ], + "type": "string" + }, + "owner": { + "description": "The owner of the repository", + "type": "string" + }, + "page": { + "description": "Page number for pagination (min 1)", + "minimum": 1, + "type": "number" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "repo": { + "description": "The name of the repository", + "type": "string" + } + }, + "required": [ + "method", + "owner", + "repo", + "issue_number" + ], + "type": "object" + }, + "name": "issue_read" + }, + { + "annotations": { + "read_only": false + }, + "description": "Create a new or update an existing issue in a GitHub repository.", + "input_schema": { + "properties": { + "assignees": { + "description": "Usernames to assign to this issue", + "items": { + "type": "string" + }, + "type": "array" + }, + "body": { + "description": "Issue body content", + "type": "string" + }, + "duplicate_of": { + "description": "Issue number that this issue is a duplicate of. Only used when state_reason is 'duplicate'.", + "type": "number" + }, + "issue_number": { + "description": "Issue number to update", + "type": "number" + }, + "labels": { + "description": "Labels to apply to this issue", + "items": { + "type": "string" + }, + "type": "array" + }, + "method": { + "description": "Write operation to perform on a single issue.\nOptions are: \n- 'create' - creates a new issue. \n- 'update' - updates an existing issue.\n", + "enum": [ + "create", + "update" + ], + "type": "string" + }, + "milestone": { + "description": "Milestone number", + "type": "number" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + }, + "state": { + "description": "New state", + "enum": [ + "open", + "closed" + ], + "type": "string" + }, + "state_reason": { + "description": "Reason for the state change. Ignored unless state is changed.", + "enum": [ + "completed", + "not_planned", + "duplicate" + ], + "type": "string" + }, + "title": { + "description": "Issue title", + "type": "string" + }, + "type": { + "description": "Type of this issue. Only use if the repository has issue types configured. Use list_issue_types tool to get valid type values for the organization. If the repository doesn't support issue types, omit this parameter.", + "type": "string" + } + }, + "required": [ + "method", + "owner", + "repo" + ], + "type": "object" + }, + "name": "issue_write" + }, + { + "annotations": { + "read_only": true + }, + "description": "List branches in a GitHub repository", + "input_schema": { + "properties": { + "owner": { + "description": "Repository owner", + "type": "string" + }, + "page": { + "description": "Page number for pagination (min 1)", + "minimum": 1, + "type": "number" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo" + ], + "type": "object" + }, + "name": "list_branches" + }, + { + "annotations": { + "read_only": true + }, + "description": "Get list of commits of a branch in a GitHub repository. Returns at least 30 results per page by default, but can return more if specified using the perPage parameter (up to 100).", + "input_schema": { + "properties": { + "author": { + "description": "Author username or email address to filter commits by", + "type": "string" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "page": { + "description": "Page number for pagination (min 1)", + "minimum": 1, + "type": "number" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + }, + "sha": { + "description": "Commit SHA, branch or tag name to list commits of. If not provided, uses the default branch of the repository. If a commit SHA is provided, will list commits up to that SHA.", + "type": "string" + } + }, + "required": [ + "owner", + "repo" + ], + "type": "object" + }, + "name": "list_commits" + }, + { + "annotations": { + "read_only": true + }, + "description": "List supported issue types for repository owner (organization).", + "input_schema": { + "properties": { + "owner": { + "description": "The organization owner of the repository", + "type": "string" + } + }, + "required": [ + "owner" + ], + "type": "object" + }, + "name": "list_issue_types" + }, + { + "annotations": { + "read_only": true + }, + "description": "List issues in a GitHub repository. For pagination, use the 'endCursor' from the previous response's 'pageInfo' in the 'after' parameter.", + "input_schema": { + "properties": { + "after": { + "description": "Cursor for pagination. Use the endCursor from the previous page's PageInfo for GraphQL APIs.", + "type": "string" + }, + "direction": { + "description": "Order direction. If provided, the 'orderBy' also needs to be provided.", + "enum": [ + "ASC", + "DESC" + ], + "type": "string" + }, + "labels": { + "description": "Filter by labels", + "items": { + "type": "string" + }, + "type": "array" + }, + "orderBy": { + "description": "Order issues by field. If provided, the 'direction' also needs to be provided.", + "enum": [ + "CREATED_AT", + "UPDATED_AT", + "COMMENTS" + ], + "type": "string" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + }, + "since": { + "description": "Filter by date (ISO 8601 timestamp)", + "type": "string" + }, + "state": { + "description": "Filter by state, by default both open and closed issues are returned when not provided", + "enum": [ + "OPEN", + "CLOSED" + ], + "type": "string" + } + }, + "required": [ + "owner", + "repo" + ], + "type": "object" + }, + "name": "list_issues" + }, + { + "annotations": { + "read_only": true + }, + "description": "List pull requests in a GitHub repository. If the user specifies an author, then DO NOT use this tool and use the search_pull_requests tool instead.", + "input_schema": { + "properties": { + "base": { + "description": "Filter by base branch", + "type": "string" + }, + "direction": { + "description": "Sort direction", + "enum": [ + "asc", + "desc" + ], + "type": "string" + }, + "head": { + "description": "Filter by head user/org and branch", + "type": "string" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "page": { + "description": "Page number for pagination (min 1)", + "minimum": 1, + "type": "number" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + }, + "sort": { + "description": "Sort by", + "enum": [ + "created", + "updated", + "popularity", + "long-running" + ], + "type": "string" + }, + "state": { + "description": "Filter by state", + "enum": [ + "open", + "closed", + "all" + ], + "type": "string" + } + }, + "required": [ + "owner", + "repo" + ], + "type": "object" + }, + "name": "list_pull_requests" + }, + { + "annotations": { + "read_only": true + }, + "description": "List releases in a GitHub repository", + "input_schema": { + "properties": { + "owner": { + "description": "Repository owner", + "type": "string" + }, + "page": { + "description": "Page number for pagination (min 1)", + "minimum": 1, + "type": "number" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo" + ], + "type": "object" + }, + "name": "list_releases" + }, + { + "annotations": { + "read_only": true + }, + "description": "List git tags in a GitHub repository", + "input_schema": { + "properties": { + "owner": { + "description": "Repository owner", + "type": "string" + }, + "page": { + "description": "Page number for pagination (min 1)", + "minimum": 1, + "type": "number" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo" + ], + "type": "object" + }, + "name": "list_tags" + }, + { + "annotations": { + "read_only": false + }, + "description": "Merge a pull request in a GitHub repository.", + "input_schema": { + "properties": { + "commit_message": { + "description": "Extra detail for merge commit", + "type": "string" + }, + "commit_title": { + "description": "Title for merge commit", + "type": "string" + }, + "merge_method": { + "description": "Merge method", + "enum": [ + "merge", + "squash", + "rebase" + ], + "type": "string" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "pullNumber": { + "description": "Pull request number", + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "pullNumber" + ], + "type": "object" + }, + "name": "merge_pull_request" + }, + { + "annotations": { + "read_only": true + }, + "description": "Get information on a specific pull request in GitHub repository.", + "input_schema": { + "properties": { + "method": { + "description": "Action to specify what pull request data needs to be retrieved from GitHub. \nPossible options: \n 1. get - Get details of a specific pull request.\n 2. get_diff - Get the diff of a pull request.\n 3. get_status - Get status of a head commit in a pull request. This reflects status of builds and checks.\n 4. get_files - Get the list of files changed in a pull request. Use with pagination parameters to control the number of results returned.\n 5. get_review_comments - Get the review comments on a pull request. They are comments made on a portion of the unified diff during a pull request review. Use with pagination parameters to control the number of results returned.\n 6. get_reviews - Get the reviews on a pull request. When asked for review comments, use get_review_comments method.\n 7. get_comments - Get comments on a pull request. Use this if user doesn't specifically want review comments. Use with pagination parameters to control the number of results returned.\n", + "enum": [ + "get", + "get_diff", + "get_status", + "get_files", + "get_review_comments", + "get_reviews", + "get_comments" + ], + "type": "string" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "page": { + "description": "Page number for pagination (min 1)", + "minimum": 1, + "type": "number" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "pullNumber": { + "description": "Pull request number", + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "method", + "owner", + "repo", + "pullNumber" + ], + "type": "object" + }, + "name": "pull_request_read" + }, + { + "annotations": { + "read_only": false + }, + "description": "Create and/or submit, delete review of a pull request.\n\nAvailable methods:\n- create: Create a new review of a pull request. If \"event\" parameter is provided, the review is submitted. If \"event\" is omitted, a pending review is created.\n- submit_pending: Submit an existing pending review of a pull request. This requires that a pending review exists for the current user on the specified pull request. The \"body\" and \"event\" parameters are used when submitting the review.\n- delete_pending: Delete an existing pending review of a pull request. This requires that a pending review exists for the current user on the specified pull request.\n", + "input_schema": { + "properties": { + "body": { + "description": "Review comment text", + "type": "string" + }, + "commitID": { + "description": "SHA of commit to review", + "type": "string" + }, + "event": { + "description": "Review action to perform.", + "enum": [ + "APPROVE", + "REQUEST_CHANGES", + "COMMENT" + ], + "type": "string" + }, + "method": { + "description": "The write operation to perform on pull request review.", + "enum": [ + "create", + "submit_pending", + "delete_pending" + ], + "type": "string" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "pullNumber": { + "description": "Pull request number", + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "method", + "owner", + "repo", + "pullNumber" + ], + "type": "object" + }, + "name": "pull_request_review_write" + }, + { + "annotations": { + "read_only": false + }, + "description": "Push multiple files to a GitHub repository in a single commit", + "input_schema": { + "properties": { + "branch": { + "description": "Branch to push to", + "type": "string" + }, + "files": { + "description": "Array of file objects to push, each object with path (string) and content (string)", + "items": { + "additionalProperties": false, + "properties": { + "content": { + "description": "file content", + "type": "string" + }, + "path": { + "description": "path to the file", + "type": "string" + } + }, + "required": [ + "path", + "content" + ], + "type": "object" + }, + "type": "array" + }, + "message": { + "description": "Commit message", + "type": "string" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "branch", + "files", + "message" + ], + "type": "object" + }, + "name": "push_files" + }, + { + "annotations": { + "read_only": false + }, + "description": "Request a GitHub Copilot code review for a pull request. Use this for automated feedback on pull requests, usually before requesting a human reviewer.", + "input_schema": { + "properties": { + "owner": { + "description": "Repository owner", + "type": "string" + }, + "pullNumber": { + "description": "Pull request number", + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "pullNumber" + ], + "type": "object" + }, + "name": "request_copilot_review" + }, + { + "annotations": { + "read_only": true + }, + "description": "Fast and precise code search across ALL GitHub repositories using GitHub's native search engine. Best for finding exact symbols, functions, classes, or specific code patterns.", + "input_schema": { + "properties": { + "order": { + "description": "Sort order for results", + "enum": [ + "asc", + "desc" + ], + "type": "string" + }, + "page": { + "description": "Page number for pagination (min 1)", + "minimum": 1, + "type": "number" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "query": { + "description": "Search query using GitHub's powerful code search syntax. Examples: 'content:Skill language:Java org:github', 'NOT is:archived language:Python OR language:go', 'repo:github/github-mcp-server'. Supports exact matching, language filters, path filters, and more.", + "type": "string" + }, + "sort": { + "description": "Sort field ('indexed' only)", + "type": "string" + } + }, + "required": [ + "query" + ], + "type": "object" + }, + "name": "search_code" + }, + { + "annotations": { + "read_only": true + }, + "description": "Search for issues in GitHub repositories using issues search syntax already scoped to is:issue", + "input_schema": { + "properties": { + "order": { + "description": "Sort order", + "enum": [ + "asc", + "desc" + ], + "type": "string" + }, + "owner": { + "description": "Optional repository owner. If provided with repo, only issues for this repository are listed.", + "type": "string" + }, + "page": { + "description": "Page number for pagination (min 1)", + "minimum": 1, + "type": "number" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "query": { + "description": "Search query using GitHub issues search syntax", + "type": "string" + }, + "repo": { + "description": "Optional repository name. If provided with owner, only issues for this repository are listed.", + "type": "string" + }, + "sort": { + "description": "Sort field by number of matches of categories, defaults to best match", + "enum": [ + "comments", + "reactions", + "reactions-+1", + "reactions--1", + "reactions-smile", + "reactions-thinking_face", + "reactions-heart", + "reactions-tada", + "interactions", + "created", + "updated" + ], + "type": "string" + } + }, + "required": [ + "query" + ], + "type": "object" + }, + "name": "search_issues" + }, + { + "annotations": { + "read_only": true + }, + "description": "Search for pull requests in GitHub repositories using issues search syntax already scoped to is:pr", + "input_schema": { + "properties": { + "order": { + "description": "Sort order", + "enum": [ + "asc", + "desc" + ], + "type": "string" + }, + "owner": { + "description": "Optional repository owner. If provided with repo, only pull requests for this repository are listed.", + "type": "string" + }, + "page": { + "description": "Page number for pagination (min 1)", + "minimum": 1, + "type": "number" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "query": { + "description": "Search query using GitHub pull request search syntax", + "type": "string" + }, + "repo": { + "description": "Optional repository name. If provided with owner, only pull requests for this repository are listed.", + "type": "string" + }, + "sort": { + "description": "Sort field by number of matches of categories, defaults to best match", + "enum": [ + "comments", + "reactions", + "reactions-+1", + "reactions--1", + "reactions-smile", + "reactions-thinking_face", + "reactions-heart", + "reactions-tada", + "interactions", + "created", + "updated" + ], + "type": "string" + } + }, + "required": [ + "query" + ], + "type": "object" + }, + "name": "search_pull_requests" + }, + { + "annotations": { + "read_only": true + }, + "description": "Find GitHub repositories by name, description, readme, topics, or other metadata. Perfect for discovering projects, finding examples, or locating specific repositories across GitHub.", + "input_schema": { + "properties": { + "minimal_output": { + "default": true, + "description": "Return minimal repository information (default: true). When false, returns full GitHub API repository objects.", + "type": "boolean" + }, + "order": { + "description": "Sort order", + "enum": [ + "asc", + "desc" + ], + "type": "string" + }, + "page": { + "description": "Page number for pagination (min 1)", + "minimum": 1, + "type": "number" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "query": { + "description": "Repository search query. Examples: 'machine learning in:name stars:>1000 language:python', 'topic:react', 'user:facebook'. Supports advanced search syntax for precise filtering.", + "type": "string" + }, + "sort": { + "description": "Sort repositories by field, defaults to best match", + "enum": [ + "stars", + "forks", + "help-wanted-issues", + "updated" + ], + "type": "string" + } + }, + "required": [ + "query" + ], + "type": "object" + }, + "name": "search_repositories" + }, + { + "annotations": { + "read_only": true + }, + "description": "Find GitHub users by username, real name, or other profile information. Useful for locating developers, contributors, or team members.", + "input_schema": { + "properties": { + "order": { + "description": "Sort order", + "enum": [ + "asc", + "desc" + ], + "type": "string" + }, + "page": { + "description": "Page number for pagination (min 1)", + "minimum": 1, + "type": "number" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "query": { + "description": "User search query. Examples: 'john smith', 'location:seattle', 'followers:>100'. Search is automatically scoped to type:user.", + "type": "string" + }, + "sort": { + "description": "Sort users by number of followers or repositories, or when the person joined GitHub.", + "enum": [ + "followers", + "repositories", + "joined" + ], + "type": "string" + } + }, + "required": [ + "query" + ], + "type": "object" + }, + "name": "search_users" + }, + { + "annotations": { + "read_only": false + }, + "description": "Add a sub-issue to a parent issue in a GitHub repository.", + "input_schema": { + "properties": { + "after_id": { + "description": "The ID of the sub-issue to be prioritized after (either after_id OR before_id should be specified)", + "type": "number" + }, + "before_id": { + "description": "The ID of the sub-issue to be prioritized before (either after_id OR before_id should be specified)", + "type": "number" + }, + "issue_number": { + "description": "The number of the parent issue", + "type": "number" + }, + "method": { + "description": "The action to perform on a single sub-issue\nOptions are:\n- 'add' - add a sub-issue to a parent issue in a GitHub repository.\n- 'remove' - remove a sub-issue from a parent issue in a GitHub repository.\n- 'reprioritize' - change the order of sub-issues within a parent issue in a GitHub repository. Use either 'after_id' or 'before_id' to specify the new position.\n\t\t\t\t", + "type": "string" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "replace_parent": { + "description": "When true, replaces the sub-issue's current parent issue. Use with 'add' method only.", + "type": "boolean" + }, + "repo": { + "description": "Repository name", + "type": "string" + }, + "sub_issue_id": { + "description": "The ID of the sub-issue to add. ID is not the same as issue number", + "type": "number" + } + }, + "required": [ + "method", + "owner", + "repo", + "issue_number", + "sub_issue_id" + ], + "type": "object" + }, + "name": "sub_issue_write" + }, + { + "annotations": { + "read_only": false + }, + "description": "Update an existing pull request in a GitHub repository.", + "input_schema": { + "properties": { + "base": { + "description": "New base branch name", + "type": "string" + }, + "body": { + "description": "New description", + "type": "string" + }, + "draft": { + "description": "Mark pull request as draft (true) or ready for review (false)", + "type": "boolean" + }, + "maintainer_can_modify": { + "description": "Allow maintainer edits", + "type": "boolean" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "pullNumber": { + "description": "Pull request number to update", + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + }, + "reviewers": { + "description": "GitHub usernames to request reviews from", + "items": { + "type": "string" + }, + "type": "array" + }, + "state": { + "description": "New state", + "enum": [ + "open", + "closed" + ], + "type": "string" + }, + "title": { + "description": "New title", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "pullNumber" + ], + "type": "object" + }, + "name": "update_pull_request" + }, + { + "annotations": { + "read_only": false + }, + "description": "Update the branch of a pull request with the latest changes from the base branch.", + "input_schema": { + "properties": { + "expectedHeadSha": { + "description": "The expected SHA of the pull request's HEAD ref", + "type": "string" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "pullNumber": { + "description": "Pull request number", + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "pullNumber" + ], + "type": "object" + }, + "name": "update_pull_request_branch" + } + ] + }, + { + "id": "rs_0bbb974cbbc378f500691e2c841370819897cf8a919e277e6a", + "type": "reasoning", + "summary": [] + }, + { + "id": "msg_0bbb974cbbc378f500691e2c925fe4819892ab87007a8a0f3a", + "type": "message", + "status": "completed", + "content": [ + { + "type": "output_text", + "annotations": [], + "logprobs": [], + "text": "Which repository do you want info for? Please provide the owner and repo name (e.g., owner/repo) or paste the GitHub URL.\n\nAlso, what details would you like? I can fetch things like:\n- Description, license, topics, default branch\n- Stars/forks/watchers\n- Latest release/tags\n- Branches and recent commits\n- Open issues/PRs and their statuses\n- Languages and file structure" + } + ], + "role": "assistant" + } + ], + "parallel_tool_calls": true, + "previous_response_id": null, + "prompt_cache_key": null, + "prompt_cache_retention": null, + "reasoning": { + "effort": "medium", + "summary": null + }, + "safety_identifier": null, + "service_tier": "default", + "store": true, + "temperature": 1.0, + "text": { + "format": { + "type": "text" + }, + "verbosity": "medium" + }, + "tool_choice": "auto", + "tools": [ + { + "type": "mcp", + "allowed_tools": null, + "authorization": "", + "headers": null, + "require_approval": "always", + "server_description": null, + "server_label": "github_copilot", + "server_url": "https://api.githubcopilot.com/mcp/" + } + ], + "top_logprobs": 0, + "top_p": 1.0, + "truncation": "disabled", + "usage": { + "input_tokens": 5312, + "input_tokens_details": { + "cached_tokens": 5248 + }, + "output_tokens": 671, + "output_tokens_details": { + "reasoning_tokens": 576 + }, + "total_tokens": 5983 + }, + "user": null, + "metadata": {} + } + recorded_at: Wed, 19 Nov 2025 20:46:12 GMT +recorded_with: VCR 6.3.1 diff --git a/test/integration/open_ai/responses/common_format/mcp_test.rb b/test/integration/open_ai/responses/common_format/mcp_test.rb new file mode 100644 index 00000000..ad6d4cb6 --- /dev/null +++ b/test/integration/open_ai/responses/common_format/mcp_test.rb @@ -0,0 +1,189 @@ +# frozen_string_literal: true + +require_relative "../../../test_helper" + +module Integration + module OpenAI + module Responses + module CommonFormat + class McpTest < ActiveSupport::TestCase + include Integration::TestHelper + + class TestAgent < ActiveAgent::Base + generate_with :openai, model: "gpt-5" + + # Single MCP server without authentication + COMMON_FORMAT_SINGLE_SERVER = { + model: "gpt-5", + input: "Get the current weather", + tools: [ + { + type: "mcp", + server_label: "weather", + server_url: "https://demo-day.mcp.cloudflare.com/sse" + } + ] + } + def common_format_single_server + prompt( + input: "Get the current weather", + mcp_servers: [ + { name: "weather", url: "https://demo-day.mcp.cloudflare.com/sse" } + ] + ) + end + + # Single MCP server with authentication + COMMON_FORMAT_SINGLE_SERVER_WITH_AUTH = { + model: "gpt-5", + input: "Get repository information", + tools: [ + { + type: "mcp", + server_label: "github_copilot", + server_url: "https://api.githubcopilot.com/mcp/", + authorization: "GITHUB_MCP_TOKEN" + } + ] + } + def common_format_single_server_with_auth + prompt( + input: "Get repository information", + mcp_servers: [ + { name: "github_copilot", url: "https://api.githubcopilot.com/mcp/", authorization: ENV["GITHUB_MCP_TOKEN"] } + ] + ) + end + + # Multiple MCP servers + COMMON_FORMAT_MULTIPLE_SERVERS = { + model: "gpt-5", + input: "Get the weather and repository information", + tools: [ + { + type: "mcp", + server_label: "weather", + server_url: "https://demo-day.mcp.cloudflare.com/sse" + }, + { + type: "mcp", + server_label: "github_copilot", + server_url: "https://api.githubcopilot.com/mcp/", + authorization: "GITHUB_MCP_TOKEN" + } + ] + } + def common_format_multiple_servers + prompt( + input: "Get the weather and repository information", + mcp_servers: [ + { name: "weather", url: "https://demo-day.mcp.cloudflare.com/sse" }, + { name: "github_copilot", url: "https://api.githubcopilot.com/mcp/", authorization: ENV["GITHUB_MCP_TOKEN"] } + ] + ) + end + + # MCP servers mixed with regular tools + COMMON_FORMAT_MIXED_TOOLS_AND_MCP = { + model: "gpt-5", + input: "Get the weather and calculate 5 + 3", + tools: [ + { + type: "function", + name: "calculate", + description: "Perform arithmetic", + parameters: { + type: "object", + properties: { + operation: { type: "string" }, + a: { type: "number" }, + b: { type: "number" } + } + } + }, + { + type: "mcp", + server_label: "weather", + server_url: "https://demo-day.mcp.cloudflare.com/sse" + } + ] + } + def common_format_mixed_tools_and_mcp + prompt( + input: "Get the weather and calculate 5 + 3", + tools: [ + { + name: "calculate", + description: "Perform arithmetic", + parameters: { + type: "object", + properties: { + operation: { type: "string" }, + a: { type: "number" }, + b: { type: "number" } + } + } + } + ], + mcp_servers: [ + { name: "weather", url: "https://demo-day.mcp.cloudflare.com/sse" } + ] + ) + end + + def calculate(operation:, a:, b:) + result = case operation + when "add" then a + b + when "subtract" then a - b + when "multiply" then a * b + when "divide" then a / b + end + { operation: operation, a: a, b: b, result: result } + end + end + + ################################################################################ + # This automatically runs all the tests for the test actions + ################################################################################ + + # Tests without sensitive tokens (can use standard test_request_builder) + [ + :common_format_single_server, + :common_format_mixed_tools_and_mcp + ].each do |action_name| + test_request_builder(TestAgent, action_name, :generate_now, TestAgent.const_get(action_name.to_s.upcase, true)) + end + + # Tests with sensitive tokens need custom handling + # These tests verify the cassette recording but skip WebMock replay verification + # because VCR filters the token in the cassette but not in the live WebMock request + [ + :common_format_single_server_with_auth, + :common_format_multiple_servers + ].each do |action_name| + agent_name = TestAgent.name.demodulize.underscore + expected_body = TestAgent.const_get(action_name.to_s.upcase) + + test "#{agent_name} #{action_name} Request Building" do + cassette_name = [ self.class.name.underscore, "#{agent_name}_#{action_name}" ].join("/") + + # Run once to record response + VCR.use_cassette(cassette_name) do + TestAgent.send(action_name).generate_now + end + + # Validate that the recorded request matches our expectations (with filtered values) + cassette_file = YAML.load_file("test/fixtures/vcr_cassettes/#{cassette_name}.yml") + saved_request_body = JSON.parse(cassette_file.dig("http_interactions", 0, "request", "body", "string"), symbolize_names: true) + + assert_equal expected_body, saved_request_body + + # Note: Skipping WebMock replay verification because VCR filters tokens in cassettes + # but WebMock sees unfiltered tokens in live requests, causing mismatches + end + end + end + end + end + end +end diff --git a/test/providers/open_ai/responses/transforms_test.rb b/test/providers/open_ai/responses/transforms_test.rb index c289e903..fe3d5df0 100644 --- a/test/providers/open_ai/responses/transforms_test.rb +++ b/test/providers/open_ai/responses/transforms_test.rb @@ -384,6 +384,82 @@ def serializable.serialize assert_equal "response", hash[:format][:name] assert hash[:format][:strict] end + + # normalize_mcp_servers tests + test "normalize_mcp_servers converts common format to OpenAI format" do + servers = [ + { name: "stripe", url: "https://mcp.stripe.com", authorization: "sk_test_123" } + ] + + result = transforms.normalize_mcp_servers(servers) + + assert_equal 1, result.size + assert_equal "mcp", result[0][:type] + assert_equal "stripe", result[0][:server_label] + assert_equal "https://mcp.stripe.com", result[0][:server_url] + assert_equal "sk_test_123", result[0][:authorization] + end + + test "normalize_mcp_servers handles multiple servers" do + servers = [ + { name: "stripe", url: "https://mcp.stripe.com", authorization: "token1" }, + { name: "github", url: "https://api.githubcopilot.com/mcp/", authorization: "token2" } + ] + + result = transforms.normalize_mcp_servers(servers) + + assert_equal 2, result.size + assert_equal "stripe", result[0][:server_label] + assert_equal "github", result[1][:server_label] + end + + test "normalize_mcp_servers handles server without authorization" do + servers = [ + { name: "public", url: "https://demo.mcp.example.com" } + ] + + result = transforms.normalize_mcp_servers(servers) + + assert_equal 1, result.size + assert_equal "mcp", result[0][:type] + assert_equal "public", result[0][:server_label] + assert_equal "https://demo.mcp.example.com", result[0][:server_url] + assert_nil result[0][:authorization] + end + + test "normalize_mcp_servers returns already normalized servers as-is" do + servers = [ + { type: "mcp", server_label: "stripe", server_url: "https://mcp.stripe.com", authorization: "token" } + ] + + result = transforms.normalize_mcp_servers(servers) + + assert_equal servers, result + end + + test "normalize_mcp_servers handles alternative field names" do + servers = [ + { server_label: "stripe", server_url: "https://mcp.stripe.com", authorization: "token" } + ] + + result = transforms.normalize_mcp_servers(servers) + + assert_equal 1, result.size + assert_equal "mcp", result[0][:type] + assert_equal "stripe", result[0][:server_label] + end + + test "normalize_mcp_servers returns nil for nil input" do + result = transforms.normalize_mcp_servers(nil) + + assert_nil result + end + + test "normalize_mcp_servers returns empty array for empty array" do + result = transforms.normalize_mcp_servers([]) + + assert_equal [], result + end end end end From e1e1a32cbfcea554a851055995c175444271d9d9 Mon Sep 17 00:00:00 2001 From: Zane Wolfgang Pickett Date: Wed, 19 Nov 2025 12:50:48 -0800 Subject: [PATCH 12/17] Add Native Format MCP Tests --- .../test_agent_mcp_server.yml | 108 ++++++++++ .../test_agent_mcp_server.yml | 199 ++++++++++++++++++ .../anthropic/native_format_test.rb | 39 +++- .../open_ai/responses/native_format_test.rb | 25 +++ 4 files changed, 370 insertions(+), 1 deletion(-) create mode 100644 test/fixtures/vcr_cassettes/integration/anthropic/native_format_test/test_agent_mcp_server.yml create mode 100644 test/fixtures/vcr_cassettes/integration/open_ai/responses/native_format_test/test_agent_mcp_server.yml diff --git a/test/fixtures/vcr_cassettes/integration/anthropic/native_format_test/test_agent_mcp_server.yml b/test/fixtures/vcr_cassettes/integration/anthropic/native_format_test/test_agent_mcp_server.yml new file mode 100644 index 00000000..a0c3a0cb --- /dev/null +++ b/test/fixtures/vcr_cassettes/integration/anthropic/native_format_test/test_agent_mcp_server.yml @@ -0,0 +1,108 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.anthropic.com/v1/messages?beta=true + body: + encoding: UTF-8 + string: '{"model":"claude-sonnet-4-5-20250929","messages":[{"content":"What + tools do you have available?","role":"user"}],"max_tokens":1024,"mcp_servers":[{"name":"cloudflare-demo","type":"url","url":"https://demo-day.mcp.cloudflare.com/sse"}]}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json + User-Agent: + - Anthropic::Client/Ruby 1.14.0 + Host: + - api.anthropic.com + X-Stainless-Arch: + - arm64 + X-Stainless-Lang: + - ruby + X-Stainless-Os: + - MacOS + X-Stainless-Package-Version: + - 1.14.0 + X-Stainless-Runtime: + - ruby + X-Stainless-Runtime-Version: + - 3.4.7 + Content-Type: + - application/json + Anthropic-Version: + - '2023-06-01' + X-Api-Key: + - ACCESS_TOKEN + Anthropic-Beta: + - mcp-client-2025-04-04 + X-Stainless-Retry-Count: + - '0' + X-Stainless-Timeout: + - '600.0' + Content-Length: + - '235' + response: + status: + code: 200 + message: OK + headers: + Date: + - Wed, 19 Nov 2025 20:50:18 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Anthropic-Ratelimit-Input-Tokens-Limit: + - '2000000' + Anthropic-Ratelimit-Input-Tokens-Remaining: + - '2000000' + Anthropic-Ratelimit-Input-Tokens-Reset: + - '2025-11-19T20:50:17Z' + Anthropic-Ratelimit-Output-Tokens-Limit: + - '400000' + Anthropic-Ratelimit-Output-Tokens-Remaining: + - '400000' + Anthropic-Ratelimit-Output-Tokens-Reset: + - '2025-11-19T20:50:18Z' + Anthropic-Ratelimit-Requests-Limit: + - '4000' + Anthropic-Ratelimit-Requests-Remaining: + - '3999' + Anthropic-Ratelimit-Requests-Reset: + - '2025-11-19T20:50:15Z' + Retry-After: + - '44' + Anthropic-Ratelimit-Tokens-Limit: + - '2400000' + Anthropic-Ratelimit-Tokens-Remaining: + - '2400000' + Anthropic-Ratelimit-Tokens-Reset: + - '2025-11-19T20:50:17Z' + Request-Id: + - req_011CVHucNXpL4ZPbJzgT2Z69 + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Anthropic-Organization-Id: + - 2557c2f2-bcfa-4054-9fa8-ae13b3b47d6b + X-Envoy-Upstream-Service-Time: + - '4865' + Cf-Cache-Status: + - DYNAMIC + X-Robots-Tag: + - none + Server: + - cloudflare + Cf-Ray: + - 9a1294248fef31f4-SJC + body: + encoding: ASCII-8BIT + string: '{"model":"claude-sonnet-4-5-20250929","id":"msg_01SoVDRqmrw4zWsNMChbZ9dZ","type":"message","role":"assistant","content":[{"type":"text","text":"I + have access to one tool:\n\n**Cloudflare MCP Demo Day Info** - This tool gets + information about Cloudflare''s MCP Demo Day. I can use it to answer questions + about Cloudflare''s MCP demo day event.\n\nIf you''d like to know more about + Cloudflare''s MCP Demo Day, I''d be happy to fetch that information for you!"}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":585,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":87,"service_tier":"standard"}}' + recorded_at: Wed, 19 Nov 2025 20:50:18 GMT +recorded_with: VCR 6.3.1 diff --git a/test/fixtures/vcr_cassettes/integration/open_ai/responses/native_format_test/test_agent_mcp_server.yml b/test/fixtures/vcr_cassettes/integration/open_ai/responses/native_format_test/test_agent_mcp_server.yml new file mode 100644 index 00000000..088e263d --- /dev/null +++ b/test/fixtures/vcr_cassettes/integration/open_ai/responses/native_format_test/test_agent_mcp_server.yml @@ -0,0 +1,199 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.openai.com/v1/responses + body: + encoding: UTF-8 + string: '{"model":"gpt-4.1","input":"What tools do you have available?","tools":[{"type":"mcp","server_label":"cloudflare-demo","server_url":"https://demo-day.mcp.cloudflare.com/sse"}]}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json + User-Agent: + - OpenAI::Client/Ruby 0.35.1 + Host: + - api.openai.com + X-Stainless-Arch: + - arm64 + X-Stainless-Lang: + - ruby + X-Stainless-Os: + - MacOS + X-Stainless-Package-Version: + - 0.35.1 + X-Stainless-Runtime: + - ruby + X-Stainless-Runtime-Version: + - 3.4.7 + Content-Type: + - application/json + Openai-Organization: + - ORGANIZATION_ID + Openai-Project: + - PROJECT_ID + Authorization: + - Bearer ACCESS_TOKEN + X-Stainless-Retry-Count: + - '0' + X-Stainless-Timeout: + - '600.0' + Content-Length: + - '176' + response: + status: + code: 200 + message: OK + headers: + Date: + - Wed, 19 Nov 2025 20:50:30 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + X-Ratelimit-Limit-Requests: + - '10000' + X-Ratelimit-Limit-Tokens: + - '30000000' + X-Ratelimit-Remaining-Requests: + - '9999' + X-Ratelimit-Remaining-Tokens: + - '29999906' + X-Ratelimit-Reset-Requests: + - 6ms + X-Ratelimit-Reset-Tokens: + - 0s + Openai-Version: + - '2020-10-01' + Openai-Organization: + - ORGANIZATION_ID + Openai-Project: + - PROJECT_ID + X-Request-Id: + - req_f930de4b1fef42efbd671c8f50d718d5 + Openai-Processing-Ms: + - '6126' + X-Envoy-Upstream-Service-Time: + - '6130' + Cf-Cache-Status: + - DYNAMIC + Set-Cookie: + - __cf_bm=CSKnilMCYDGbIJen9mOqPAZ7DhPigg8Pt9lbuzpb4C0-1763585430-1.0.1.1-elHDnP58qXxQEsSrnvucnenaNLCDZx7DPQ.RgpWQl00bHT8O.GMC2dbL7nYk0AcuEUiE0o3B_2_3Fw2X1jP5rCtTjCg9XwrOBh9NALSeSiM; + path=/; expires=Wed, 19-Nov-25 21:20:30 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=UCeoHiVlR_TvsF53s39eQ1irF_vsGIxk5sC3B_i1KBM-1763585430500-0.0.1.1-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + X-Content-Type-Options: + - nosniff + Server: + - cloudflare + Cf-Ray: + - 9a1294661dd3eb25-SJC + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: |- + { + "id": "resp_03973314476bd0d900691e2d905f88819bb815ddd4fd9ab654", + "object": "response", + "created_at": 1763585424, + "status": "completed", + "background": false, + "billing": { + "payer": "developer" + }, + "error": null, + "incomplete_details": null, + "instructions": null, + "max_output_tokens": null, + "max_tool_calls": null, + "model": "gpt-4.1-2025-04-14", + "output": [ + { + "id": "mcpl_03973314476bd0d900691e2d90b224819b91340ffbd1a64e78", + "type": "mcp_list_tools", + "server_label": "cloudflare-demo", + "tools": [ + { + "annotations": { + "read_only": false + }, + "description": "Get information about Cloudflare's MCP Demo Day. Use this tool if the user asks about Cloudflare's MCP demo day", + "input_schema": { + "type": "object", + "properties": {} + }, + "name": "mcp_demo_day_info" + } + ] + }, + { + "id": "msg_03973314476bd0d900691e2d922164819b80e5e14143fd8a75", + "type": "message", + "status": "completed", + "content": [ + { + "type": "output_text", + "annotations": [], + "logprobs": [], + "text": "Here are the tools I currently have available:\n\n1. **Image Input Capabilities:** I can analyze and interpret images you upload.\n2. **Web Browsing (not available right now):** Sometimes, I have the ability to access the web for real-time information, but it's currently disabled.\n3. **Plugin for Cloudflare's MCP Demo Day:** \n - I can fetch information about Cloudflare\u2019s Managed Components Platform (MCP) Demo Day using a dedicated plugin.\n\nIf you need information from these areas or want to try the MCP Demo Day plugin, let me know!" + } + ], + "role": "assistant" + } + ], + "parallel_tool_calls": true, + "previous_response_id": null, + "prompt_cache_key": null, + "prompt_cache_retention": null, + "reasoning": { + "effort": null, + "summary": null + }, + "safety_identifier": null, + "service_tier": "default", + "store": true, + "temperature": 1.0, + "text": { + "format": { + "type": "text" + }, + "verbosity": "medium" + }, + "tool_choice": "auto", + "tools": [ + { + "type": "mcp", + "allowed_tools": null, + "headers": null, + "require_approval": "always", + "server_description": null, + "server_label": "cloudflare-demo", + "server_url": "https://demo-day.mcp.cloudflare.com/sse" + } + ], + "top_logprobs": 0, + "top_p": 1.0, + "truncation": "disabled", + "usage": { + "input_tokens": 75, + "input_tokens_details": { + "cached_tokens": 0 + }, + "output_tokens": 119, + "output_tokens_details": { + "reasoning_tokens": 0 + }, + "total_tokens": 194 + }, + "user": null, + "metadata": {} + } + recorded_at: Wed, 19 Nov 2025 20:50:30 GMT +recorded_with: VCR 6.3.1 diff --git a/test/integration/anthropic/native_format_test.rb b/test/integration/anthropic/native_format_test.rb index 02681cda..8ea0ee34 100644 --- a/test/integration/anthropic/native_format_test.rb +++ b/test/integration/anthropic/native_format_test.rb @@ -620,6 +620,42 @@ def stop_sequences stop_sequences: [ "}" ] ) end + + ############################################################### + # Native Format MCP Server + ############################################################### + MCP_SERVER = { + model: "claude-sonnet-4-5-20250929", + messages: [ + { + role: "user", + content: "What tools do you have available?" + } + ], + max_tokens: 1024, + mcp_servers: [ + { + type: "url", + url: "https://demo-day.mcp.cloudflare.com/sse", + name: "cloudflare-demo" + } + ] + } + def mcp_server + prompt( + messages: [ + { role: "user", content: "What tools do you have available?" } + ], + max_tokens: 1024, + mcp_servers: [ + { + type: "url", + url: "https://demo-day.mcp.cloudflare.com/sse", + name: "cloudflare-demo" + } + ] + ) + end end ################################################################################ @@ -642,7 +678,8 @@ def stop_sequences :streaming, :tools_with_streaming, :sampling_parameters, - :stop_sequences + :stop_sequences, + :mcp_server ].each do |action_name| test_request_builder(TestAgent, action_name, :generate_now, TestAgent.const_get(action_name.to_s.upcase, true)) end diff --git a/test/integration/open_ai/responses/native_format_test.rb b/test/integration/open_ai/responses/native_format_test.rb index 2de74388..b89148c8 100644 --- a/test/integration/open_ai/responses/native_format_test.rb +++ b/test/integration/open_ai/responses/native_format_test.rb @@ -183,6 +183,30 @@ def get_current_weather(location:, unit: "fahrenheit") { location:, unit:, temperature: "22" } end + MCP_SERVER = { + model: "gpt-4.1", + input: "What tools do you have available?", + tools: [ + { + type: "mcp", + server_label: "cloudflare-demo", + server_url: "https://demo-day.mcp.cloudflare.com/sse" + } + ] + } + def mcp_server + prompt( + input: "What tools do you have available?", + tools: [ + { + type: "mcp", + server_label: "cloudflare-demo", + server_url: "https://demo-day.mcp.cloudflare.com/sse" + } + ] + ) + end + REASONING = { model: "o3-mini", input: "How much wood would a woodchuck chuck?", @@ -243,6 +267,7 @@ def functions_with_streaming # :file_search, :streaming, :functions, + :mcp_server, :reasoning, :functions_with_streaming ].each do |action_name| From d67d5cc2d8b3c28bc94d4854894a7ba1a7ec89b2 Mon Sep 17 00:00:00 2001 From: Zane Wolfgang Pickett Date: Wed, 19 Nov 2025 13:22:15 -0800 Subject: [PATCH 13/17] Update Docs for MCPs Common Format --- docs/.vitepress/config.mts | 1 + docs/actions.md | 6 + docs/actions/mcps.md | 88 + docs/actions/tools.md | 35 +- docs/examples/research-agent.md | 8 +- docs/providers/anthropic.md | 2 +- docs/providers/open_ai.md | 2 +- .../providers/anthropic/transforms.rb | 9 +- .../providers/open_ai/responses/request.rb | 9 +- test/docs/actions/mcps_examples_test.rb | 257 +++ test/docs/actions_examples_test.rb | 25 + .../docs/actions/mcps/anthropic_basic.yml | 110 + .../docs/actions/mcps/multiple_servers.yml | 1933 +++++++++++++++++ .../actions/mcps/native_format_anthropic.yml | 109 + .../actions/mcps/native_format_openai.yml | 1906 ++++++++++++++++ .../actions/mcps/openai_custom_servers.yml | 1906 ++++++++++++++++ .../docs/actions/mcps/quick_start_weather.yml | 112 + .../docs/actions/mcps/single_server.yml | 112 + .../docs/actions/mcps/with_function_tools.yml | 226 ++ .../docs/actions_examples/mcps.yml | 104 + .../test_agent_common_format_mixed_auth.yml | 86 +- ...t_agent_common_format_multiple_servers.yml | 91 +- ...test_agent_common_format_single_server.yml | 32 +- ..._common_format_single_server_with_auth.yml | 95 +- .../test_agent_common_format_sse_server.yml | 32 +- .../test_agent_mcp_server.yml | 32 +- ...gent_common_format_mixed_tools_and_mcp.yml | 74 +- ...t_agent_common_format_multiple_servers.yml | 42 +- ...test_agent_common_format_single_server.yml | 38 +- ..._common_format_single_server_with_auth.yml | 40 +- .../test_agent_mcp_server.yml | 32 +- .../responses/common_format/mcp_test.rb | 8 +- 32 files changed, 7214 insertions(+), 348 deletions(-) create mode 100644 docs/actions/mcps.md create mode 100644 test/docs/actions/mcps_examples_test.rb create mode 100644 test/fixtures/vcr_cassettes/docs/actions/mcps/anthropic_basic.yml create mode 100644 test/fixtures/vcr_cassettes/docs/actions/mcps/multiple_servers.yml create mode 100644 test/fixtures/vcr_cassettes/docs/actions/mcps/native_format_anthropic.yml create mode 100644 test/fixtures/vcr_cassettes/docs/actions/mcps/native_format_openai.yml create mode 100644 test/fixtures/vcr_cassettes/docs/actions/mcps/openai_custom_servers.yml create mode 100644 test/fixtures/vcr_cassettes/docs/actions/mcps/quick_start_weather.yml create mode 100644 test/fixtures/vcr_cassettes/docs/actions/mcps/single_server.yml create mode 100644 test/fixtures/vcr_cassettes/docs/actions/mcps/with_function_tools.yml create mode 100644 test/fixtures/vcr_cassettes/docs/actions_examples/mcps.yml diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 2b878f84..98b8a317 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -116,6 +116,7 @@ export default defineConfig({ { text: 'Messages', link: '/actions/messages' }, { text: 'Embeddings', link: '/actions/embeddings' }, { text: 'Tools', link: '/actions/tools' }, + { text: 'MCPs', link: '/actions/mcps' }, { text: 'Structured Output', link: '/actions/structured_output' }, { text: 'Usage', link: '/actions/usage' }, ] diff --git a/docs/actions.md b/docs/actions.md index 22e6bc5d..c2e774d8 100644 --- a/docs/actions.md +++ b/docs/actions.md @@ -31,6 +31,12 @@ Let AI call Ruby methods during generation: <<< @/../test/docs/actions_examples_test.rb#tools_weather_agent{ruby:line-numbers} +### [MCPs](/actions/mcps) + +Connect to external services via Model Context Protocol: + +<<< @/../test/docs/actions_examples_test.rb#mcps_research_agent{ruby:line-numbers} + ### [Structured Output](/actions/structured_output) Enforce JSON responses with schemas: diff --git a/docs/actions/mcps.md b/docs/actions/mcps.md new file mode 100644 index 00000000..0ccff44e --- /dev/null +++ b/docs/actions/mcps.md @@ -0,0 +1,88 @@ +--- +title: Model Context Protocols (MCP) +description: Connect agents to external services and APIs using the Model Context Protocol. Universal integration for tools and data sources. +--- +# {{ $frontmatter.title }} + +Connect agents to external services via [Model Context Protocol](https://modelcontextprotocol.io/) servers. MCP servers expose tools and data sources that agents can use automatically. + +## Quick Start + +<<< @/../test/docs/actions/mcps_examples_test.rb#quick_start_weather_agent {ruby:line-numbers} + +## Provider Support + +| Provider | Support | Notes | +|:---------------|:-------:|:------| +| **OpenAI** | ✅ | Via Responses API | +| **Anthropic** | ⚠️ | Beta | +| **OpenRouter** | 🚧 | In development | +| **Ollama** | ❌ | Not supported | +| **Mock** | ❌ | Not supported | + +## MCP Format + +```ruby +{ + name: "server_name", # Required: server identifier + url: "https://server.url", # Required: MCP endpoint + authorization: "token" # Optional: auth token +} +``` + +### Single Server + +<<< @/../test/docs/actions/mcps_examples_test.rb#single_server_data_agent {ruby:line-numbers} + +### Multiple Servers + +<<< @/../test/docs/actions/mcps_examples_test.rb#multiple_servers_integrated_agent {ruby:line-numbers} + +### With Function Tools + +<<< @/../test/docs/actions/mcps_examples_test.rb#hybrid_agent_with_tools {ruby:line-numbers} + +## OpenAI + +OpenAI supports MCP via the Responses API with pre-built connectors and custom servers. + +### Pre-built Connectors + +<<< @/../test/docs/actions/mcps_examples_test.rb#openai_prebuilt_connectors {ruby:line-numbers} + +Available: Dropbox, Google Drive, GitHub, Slack, and more. See [OpenAI's MCP docs](https://platform.openai.com/docs/guides/mcp) for the full list. + +### Custom Servers + +<<< @/../test/docs/actions/mcps_examples_test.rb#openai_custom_servers {ruby:line-numbers} + +## Anthropic + +Anthropic supports MCP servers via the `mcp_servers` parameter (beta). Up to 20 servers per request. + +<<< @/../test/docs/actions/mcps_examples_test.rb#anthropic_basic_mcp {ruby:line-numbers} + +See [Anthropic's MCP docs](https://docs.anthropic.com/en/docs/build-with-claude/mcp) for details. + +## Native Formats + +ActiveAgent converts the common format to provider-specific formats automatically. Use native formats only if needed for provider-specific features. + +::: code-group +<<< @/../test/docs/actions/mcps_examples_test.rb#native_formats_openai {ruby:line-numbers} [OpenAI] +<<< @/../test/docs/actions/mcps_examples_test.rb#native_formats_anthropic {ruby:line-numbers} [Anthropic] +::: + +## Troubleshooting + +**Server not responding:** Verify the URL is correct and accessible from your environment. + +**Authorization failures:** Check token validity, permissions, and expiration. + +**Tools not available:** Ensure the server implements MCP correctly and returns valid tool definitions. + +## Related + +- [Tools](/actions/tools) - Function tools and tool choice +- [OpenAI Provider](/providers/open_ai) - OpenAI-specific features +- [Anthropic Provider](/providers/anthropic) - Anthropic-specific features diff --git a/docs/actions/tools.md b/docs/actions/tools.md index 0a040282..61f51435 100644 --- a/docs/actions/tools.md +++ b/docs/actions/tools.md @@ -17,13 +17,15 @@ The LLM calls `get_weather` automatically when it needs weather data, and uses t ## Provider Support Matrix -| Provider | Functions | Server-side Tools | MCP Support | Notes | -|:---------------|:---------:|:-----------------:|:-----------:|:------| -| **OpenAI** | 🟩 | 🟩 | 🟩 | Server-side tools and MCP require Responses API | -| **Anthropic** | 🟩 | 🟩 | 🟨 | MCP in beta | -| **OpenRouter** | 🟩 | ❌ | 🟦 | MCP via converted tool definitions; model-dependent capabilities | -| **Ollama** | 🟩 | ❌ | ❌ | Model-dependent capabilities | -| **Mock** | 🟦 | ❌ | ❌ | Accepted but not enforced | +| Provider | Functions | Server-side Tools | Notes | +|:---------------|:---------:|:-----------------:|:------| +| **OpenAI** | 🟩 | 🟩 | Server-side tools require Responses API | +| **Anthropic** | 🟩 | 🟩 | Full support for built-in tools | +| **OpenRouter** | 🟩 | ❌ | Model-dependent capabilities | +| **Ollama** | 🟩 | ❌ | Model-dependent capabilities | +| **Mock** | 🟦 | ❌ | Accepted but not enforced | + +For **MCP (Model Context Protocol)** support, see the [MCP documentation](/actions/mcps). ## Functions @@ -131,24 +133,6 @@ OpenAI's **Responses API** provides several built-in tools (requires GPT-5, GPT- Anthropic provides web access and specialized capabilities including Web Search for real-time information, Web Fetch (Beta) for specific URLs, Extended Thinking to show reasoning processes, and Computer Use (Beta) for interface interaction. For complete details and examples, see [Anthropic's tool use documentation](https://docs.claude.com/en/docs/agents-and-tools/tool-use/overview). -## Model Context Protocol (MCP) - -MCP (Model Context Protocol) enables agents to connect to external services and APIs. Think of it as a universal adapter for integrating tools and data sources. - -### OpenAI MCP Integration - -OpenAI supports MCP through their Responses API in two ways: pre-built connectors for popular services (Dropbox, Google Drive, GitHub, Slack, and more) and custom MCP servers. For complete details on OpenAI's MCP support, connector IDs, and configuration options, see [OpenAI's MCP documentation](https://platform.openai.com/docs/guides/mcp). - -### Anthropic MCP Integration - -Anthropic supports MCP servers via the `mcp_servers` parameter (beta feature). You can connect up to 20 MCP servers per request. For the latest on Anthropic's MCP implementation and configuration, see [Anthropic's MCP documentation](https://docs.anthropic.com/en/docs/build-with-claude/mcp). - -### OpenRouter MCP Integration - -::: info Coming Soon -MCP support for OpenRouter is currently under development and will be available in a future release. -::: - ## Troubleshooting ### Tool Not Being Called @@ -161,6 +145,7 @@ If the LLM passes unexpected parameters, add detailed parameter descriptions wit ## Related Documentation +- [MCP (Model Context Protocol)](/actions/mcps) - Connect to external services via MCP - [Agents](/agents) - Understand the agent lifecycle and callbacks - [Generation](/agents/generation) - Execute tool-enabled generations - [Messages](/actions/messages) - Learn about conversation structure diff --git a/docs/examples/research-agent.md b/docs/examples/research-agent.md index a7984633..3f33d9c7 100644 --- a/docs/examples/research-agent.md +++ b/docs/examples/research-agent.md @@ -43,7 +43,7 @@ class ResearchAgent < ApplicationAgent # Configure research tools at the class level configure_research_tools( enable_web_search: true, - mcp_servers: ["arxiv", "github"], + mcps: ["arxiv", "github"], default_search_context: "high" ) @@ -253,7 +253,7 @@ Configure default research settings: class ResearchAgent < ApplicationAgent configure_research_tools( enable_web_search: true, - mcp_servers: ["arxiv", "github", "pubmed"], + mcps: ["arxiv", "github", "pubmed"], default_search_context: "high", enable_visualizations: true ) @@ -399,7 +399,7 @@ class ResearchAgent < ApplicationAgent configure_research_tools( enable_web_search: true, - mcp_servers: ["arxiv"] + mcps: ["arxiv"] ) end @@ -408,7 +408,7 @@ class AcademicAgent < ApplicationAgent configure_research_tools( enable_web_search: false, - mcp_servers: ["arxiv", "pubmed"] + mcps: ["arxiv", "pubmed"] ) end ``` diff --git a/docs/providers/anthropic.md b/docs/providers/anthropic.md index c1d82e5c..c5698fbd 100644 --- a/docs/providers/anthropic.md +++ b/docs/providers/anthropic.md @@ -107,7 +107,7 @@ Anthropic provides access to the Claude model family. For the complete list of a - **`thinking`** - Enable Claude's thinking mode for complex reasoning - **`context_management`** - Configure context window management - **`service_tier`** - Select service tier ("auto", "standard_only") -- **`mcp_servers`** - Array of MCP server definitions (max 20) +- **`mcps`** - Array of MCP server definitions (max 20) ### Client Configuration diff --git a/docs/providers/open_ai.md b/docs/providers/open_ai.md index ce88dd34..2033bff8 100644 --- a/docs/providers/open_ai.md +++ b/docs/providers/open_ai.md @@ -120,7 +120,7 @@ Search-preview models in Chat API provide web search but with different configur - `mcp` - Enable MCP integration - **`tool_choice`** - Control tool usage ("auto", "required", "none", or specific tool) - **`parallel_tool_calls`** - Allow parallel tool execution (boolean) -- **`mcp_servers`** - Array of MCP server configurations (max 20) +- **`mcps`** - Array of MCP server configurations (max 20) ### Embeddings diff --git a/lib/active_agent/providers/anthropic/transforms.rb b/lib/active_agent/providers/anthropic/transforms.rb index 8214f845..a2b2aead 100644 --- a/lib/active_agent/providers/anthropic/transforms.rb +++ b/lib/active_agent/providers/anthropic/transforms.rb @@ -30,7 +30,14 @@ def normalize_params(params) params[:system] = normalize_system(params[:system]) if params[:system] params[:tools] = normalize_tools(params[:tools]) if params[:tools] params[:tool_choice] = normalize_tool_choice(params[:tool_choice]) if params[:tool_choice] - params[:mcp_servers] = normalize_mcp_servers(params[:mcp_servers]) if params[:mcp_servers] + + # Handle mcps parameter (common format) -> transforms to mcp_servers (provider format) + if params[:mcps] + params[:mcp_servers] = normalize_mcp_servers(params.delete(:mcps)) + elsif params[:mcp_servers] + params[:mcp_servers] = normalize_mcp_servers(params[:mcp_servers]) + end + params end diff --git a/lib/active_agent/providers/open_ai/responses/request.rb b/lib/active_agent/providers/open_ai/responses/request.rb index dd5cdf57..4566c83d 100644 --- a/lib/active_agent/providers/open_ai/responses/request.rb +++ b/lib/active_agent/providers/open_ai/responses/request.rb @@ -77,10 +77,13 @@ def initialize(**params) params[:tools] = Responses::Transforms.normalize_tools(params[:tools]) if params[:tools] params[:tool_choice] = Responses::Transforms.normalize_tool_choice(params[:tool_choice]) if params[:tool_choice] - # Step 8: Normalize MCP servers from common format + # Step 8: Normalize MCP servers from common format (mcps parameter) # OpenAI treats MCP servers as a special type of tool in the tools array - if params[:mcp_servers]&.any? - normalized_mcp_tools = Responses::Transforms.normalize_mcp_servers(params.delete(:mcp_servers)) + mcp_param = params[:mcps] || params[:mcp_servers] + if mcp_param&.any? + normalized_mcp_tools = Responses::Transforms.normalize_mcp_servers(mcp_param) + params.delete(:mcps) + params.delete(:mcp_servers) # Merge MCP servers into tools array params[:tools] = (params[:tools] || []) + normalized_mcp_tools end diff --git a/test/docs/actions/mcps_examples_test.rb b/test/docs/actions/mcps_examples_test.rb new file mode 100644 index 00000000..2a4d959b --- /dev/null +++ b/test/docs/actions/mcps_examples_test.rb @@ -0,0 +1,257 @@ +require "test_helper" + +module Docs + module Actions + class McpsExamplesTest < ActiveSupport::TestCase + class QuickStartExample < ActiveSupport::TestCase + # region quick_start_weather_agent + class WeatherAgent < ActiveAgent::Base + generate_with :anthropic, model: "claude-haiku-4-5" + + def forecast + prompt( + message: "What's the weather like?", + mcps: [ { name: "weather", url: "https://demo-day.mcp.cloudflare.com/sse" } ] + ) + end + end + # endregion quick_start_weather_agent + + test "quick start weather agent with mcps" do + VCR.use_cassette("docs/actions/mcps/quick_start_weather") do + response = WeatherAgent.forecast.generate_now + + assert response.message.content.present? + + doc_example_output(response) + end + end + end + + class SingleServerExample < ActiveSupport::TestCase + # region single_server_data_agent + class DataAgent < ActiveAgent::Base + generate_with :anthropic, model: "claude-haiku-4-5" + + def analyze + prompt( + message: "Analyze the latest data", + mcps: [ { name: "cloudflare-demo", url: "https://demo-day.mcp.cloudflare.com/sse" } ] + ) + end + end + # endregion single_server_data_agent + + test "single server MCP connection" do + VCR.use_cassette("docs/actions/mcps/single_server") do + response = DataAgent.analyze.generate_now + + assert response.message.content.present? + + doc_example_output(response) + end + end + end + + class MultipleServersExample < ActiveSupport::TestCase + # region multiple_servers_integrated_agent + class IntegratedAgent < ActiveAgent::Base + generate_with :openai, model: "gpt-4o" + + def research + prompt( + input: "Research the latest AI developments", + mcps: [ + { name: "cloudflare", url: "https://demo-day.mcp.cloudflare.com/sse" }, + { name: "github", url: "https://api.githubcopilot.com/mcp/", authorization: ENV["GITHUB_MCP_TOKEN"] } + ] + ) + end + end + # endregion multiple_servers_integrated_agent + + test "multiple MCP servers connection" do + VCR.use_cassette("docs/actions/mcps/multiple_servers") do + response = IntegratedAgent.research.generate_now + + assert response.message.content.present? + + doc_example_output(response) + end + end + end + + class WithFunctionToolsExample < ActiveSupport::TestCase + # region hybrid_agent_with_tools + class HybridAgent < ActiveAgent::Base + generate_with :openai, model: "gpt-4o" + + def analyze_data + prompt( + input: "Calculate and fetch data", + tools: [ { + name: "calculate", + description: "Perform calculations", + parameters: { + type: "object", + properties: { + operation: { type: "string" }, + a: { type: "number" }, + b: { type: "number" } + } + } + } ], + mcps: [ { name: "data-service", url: "https://demo-day.mcp.cloudflare.com/sse" } ] + ) + end + + def calculate(operation:, a:, b:) + case operation + when "add" then a + b + when "subtract" then a - b + end + end + end + # endregion hybrid_agent_with_tools + + test "MCP with function tools" do + VCR.use_cassette("docs/actions/mcps/with_function_tools") do + response = HybridAgent.analyze_data.generate_now + + assert response.message.content.present? + + doc_example_output(response) + end + end + end + + class OpenAIPrebuiltConnectorsExample < ActiveSupport::TestCase + # region openai_prebuilt_connectors + class FileAgent < ActiveAgent::Base + generate_with :openai, model: "gpt-4o" + + def search_files + prompt( + input: "Find documents about Q4 revenue", + mcps: [ { name: "dropbox", url: "mcp://dropbox" } ] # Pre-built connector + ) + end + end + # endregion openai_prebuilt_connectors + + test "OpenAI pre-built connectors" do + skip "Pre-built connectors require real OAuth tokens" + # This example is for documentation purposes only + # Real testing would require actual Dropbox OAuth setup + end + end + + class OpenAICustomServersExample < ActiveSupport::TestCase + # region openai_custom_servers + class CustomAgent < ActiveAgent::Base + generate_with :openai, model: "gpt-4o" + + def custom_tools + prompt( + input: "Use custom tools", + mcps: [ { name: "github_copilot", url: "https://api.githubcopilot.com/mcp/", authorization: ENV["GITHUB_MCP_TOKEN"] } ] + ) + end + end + # endregion openai_custom_servers + + test "OpenAI custom MCP servers" do + VCR.use_cassette("docs/actions/mcps/openai_custom_servers") do + response = CustomAgent.custom_tools.generate_now + + assert response.message.content.present? + + doc_example_output(response) + end + end + end + + class AnthropicBasicExample < ActiveSupport::TestCase + # region anthropic_basic_mcp + class ClaudeAgent < ActiveAgent::Base + generate_with :anthropic, model: "claude-haiku-4-5" + + def use_mcp + prompt( + message: "What tools are available?", + mcps: [ { name: "demo-server", url: "https://demo-day.mcp.cloudflare.com/sse" } ] + ) + end + end + # endregion anthropic_basic_mcp + + test "Anthropic basic MCP usage" do + VCR.use_cassette("docs/actions/mcps/anthropic_basic") do + response = ClaudeAgent.use_mcp.generate_now + + assert response.message.content.present? + + doc_example_output(response) + end + end + end + + class NativeFormatsExample < ActiveSupport::TestCase + # region native_formats_openai + class OpenAINativeAgent < ActiveAgent::Base + generate_with :openai, model: "gpt-4o" + + def native_format + prompt( + input: "What can you do?", + tools: [ { + type: "mcp", + server_label: "github", + server_url: "https://api.githubcopilot.com/mcp/", + authorization: ENV["GITHUB_MCP_TOKEN"] + } ] + ) + end + end + # endregion native_formats_openai + + # region native_formats_anthropic + class AnthropicNativeAgent < ActiveAgent::Base + generate_with :anthropic, model: "claude-haiku-4-5" + + def native_format + prompt( + message: "What can you do?", + mcp_servers: [ { + type: "url", + name: "cloudflare", + url: "https://demo-day.mcp.cloudflare.com/sse" + } ] + ) + end + end + # endregion native_formats_anthropic + + test "native format OpenAI" do + VCR.use_cassette("docs/actions/mcps/native_format_openai") do + response = OpenAINativeAgent.native_format.generate_now + + assert response.message.content.present? + + doc_example_output(response) + end + end + + test "native format Anthropic" do + VCR.use_cassette("docs/actions/mcps/native_format_anthropic") do + response = AnthropicNativeAgent.native_format.generate_now + + assert response.message.content.present? + + doc_example_output(response) + end + end + end + end + end +end diff --git a/test/docs/actions_examples_test.rb b/test/docs/actions_examples_test.rb index f9d0b590..ad439fdc 100644 --- a/test/docs/actions_examples_test.rb +++ b/test/docs/actions_examples_test.rb @@ -92,6 +92,31 @@ def get_current_weather(location:, unit: "fahrenheit") end end + class McpsExample < ActiveSupport::TestCase + # region mcps_research_agent + class ResearchAgent < ApplicationAgent + generate_with :anthropic, model: "claude-haiku-4-5" + + def research + prompt( + message: "Research AI developments", + mcps: [ { name: "github", url: "https://api.githubcopilot.com/mcp/", authorization: ENV["GITHUB_MCP_TOKEN"] } ] + ) + end + end + # endregion mcps_research_agent + + test "MCP connection" do + VCR.use_cassette("docs/actions_examples/mcps") do + response = ResearchAgent.research.generate_now + + assert response.message.content.present? + + doc_example_output(response) + end + end + end + # region structured_output_extract class DataExtractionAgent < ApplicationAgent generate_with :openai, model: "gpt-4o" diff --git a/test/fixtures/vcr_cassettes/docs/actions/mcps/anthropic_basic.yml b/test/fixtures/vcr_cassettes/docs/actions/mcps/anthropic_basic.yml new file mode 100644 index 00000000..a36ba591 --- /dev/null +++ b/test/fixtures/vcr_cassettes/docs/actions/mcps/anthropic_basic.yml @@ -0,0 +1,110 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.anthropic.com/v1/messages?beta=true + body: + encoding: UTF-8 + string: '{"model":"claude-haiku-4-5","messages":[{"content":"What tools are + available?","role":"user"}],"max_tokens":4096,"mcp_servers":[{"name":"demo-server","type":"url","url":"https://demo-day.mcp.cloudflare.com/sse"}]}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json + User-Agent: + - Anthropic::Client/Ruby 1.14.0 + Host: + - api.anthropic.com + X-Stainless-Arch: + - arm64 + X-Stainless-Lang: + - ruby + X-Stainless-Os: + - MacOS + X-Stainless-Package-Version: + - 1.14.0 + X-Stainless-Runtime: + - ruby + X-Stainless-Runtime-Version: + - 3.4.7 + Content-Type: + - application/json + Anthropic-Version: + - '2023-06-01' + X-Api-Key: + - ACCESS_TOKEN + Anthropic-Beta: + - mcp-client-2025-04-04 + X-Stainless-Retry-Count: + - '0' + X-Stainless-Timeout: + - '600.0' + Content-Length: + - '213' + response: + status: + code: 200 + message: OK + headers: + Date: + - Wed, 19 Nov 2025 21:18:51 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Anthropic-Ratelimit-Input-Tokens-Limit: + - '4000000' + Anthropic-Ratelimit-Input-Tokens-Remaining: + - '4000000' + Anthropic-Ratelimit-Input-Tokens-Reset: + - '2025-11-19T21:18:50Z' + Anthropic-Ratelimit-Output-Tokens-Limit: + - '800000' + Anthropic-Ratelimit-Output-Tokens-Remaining: + - '800000' + Anthropic-Ratelimit-Output-Tokens-Reset: + - '2025-11-19T21:18:51Z' + Anthropic-Ratelimit-Requests-Limit: + - '4000' + Anthropic-Ratelimit-Requests-Remaining: + - '3999' + Anthropic-Ratelimit-Requests-Reset: + - '2025-11-19T21:18:50Z' + Retry-After: + - '10' + Anthropic-Ratelimit-Tokens-Limit: + - '4800000' + Anthropic-Ratelimit-Tokens-Remaining: + - '4800000' + Anthropic-Ratelimit-Tokens-Reset: + - '2025-11-19T21:18:50Z' + Request-Id: + - req_011CVHwnnL48nmgHJDW47Snx + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Anthropic-Organization-Id: + - 2557c2f2-bcfa-4054-9fa8-ae13b3b47d6b + X-Envoy-Upstream-Service-Time: + - '3100' + Cf-Cache-Status: + - DYNAMIC + X-Robots-Tag: + - none + Server: + - cloudflare + Cf-Ray: + - 9a12be02bb591f2f-SJC + body: + encoding: ASCII-8BIT + string: '{"model":"claude-haiku-4-5-20251001","id":"msg_01EAYgadXov5fnUqr13pDWuT","type":"message","role":"assistant","content":[{"type":"text","text":"Based + on the tools I have access to, here''s what''s available:\n\n1. **demo-server_mcp_demo_day_info** + - Get information about Cloudflare''s MCP Demo Day\n - This tool allows + you to retrieve details about Cloudflare''s MCP (Model Context Protocol) demo + day event\n - No parameters required\n\nThat''s the tool currently available + in my environment. If you''d like to learn more about Cloudflare''s MCP Demo + Day, I can fetch that information for you!"}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":581,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":117,"service_tier":"standard"}}' + recorded_at: Wed, 19 Nov 2025 21:18:52 GMT +recorded_with: VCR 6.3.1 diff --git a/test/fixtures/vcr_cassettes/docs/actions/mcps/multiple_servers.yml b/test/fixtures/vcr_cassettes/docs/actions/mcps/multiple_servers.yml new file mode 100644 index 00000000..d69c8d1d --- /dev/null +++ b/test/fixtures/vcr_cassettes/docs/actions/mcps/multiple_servers.yml @@ -0,0 +1,1933 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.openai.com/v1/responses + body: + encoding: UTF-8 + string: '{"model":"gpt-4o","input":"Research the latest AI developments","tools":[{"type":"mcp","server_label":"cloudflare","server_url":"https://demo-day.mcp.cloudflare.com/sse"},{"type":"mcp","server_label":"github","server_url":"https://api.githubcopilot.com/mcp/","authorization":"GITHUB_MCP_TOKEN"}]}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json + User-Agent: + - OpenAI::Client/Ruby 0.35.1 + Host: + - api.openai.com + X-Stainless-Arch: + - arm64 + X-Stainless-Lang: + - ruby + X-Stainless-Os: + - MacOS + X-Stainless-Package-Version: + - 0.35.1 + X-Stainless-Runtime: + - ruby + X-Stainless-Runtime-Version: + - 3.4.7 + Content-Type: + - application/json + Openai-Organization: + - ORGANIZATION_ID + Openai-Project: + - PROJECT_ID + Authorization: + - Bearer ACCESS_TOKEN + X-Stainless-Retry-Count: + - '0' + X-Stainless-Timeout: + - '600.0' + Content-Length: + - '373' + response: + status: + code: 200 + message: OK + headers: + Date: + - Wed, 19 Nov 2025 21:19:03 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + X-Ratelimit-Limit-Requests: + - '10000' + X-Ratelimit-Limit-Tokens: + - '30000000' + X-Ratelimit-Remaining-Requests: + - '9999' + X-Ratelimit-Remaining-Tokens: + - '29994688' + X-Ratelimit-Reset-Requests: + - 6ms + X-Ratelimit-Reset-Tokens: + - 10ms + Openai-Version: + - '2020-10-01' + Openai-Organization: + - ORGANIZATION_ID + Openai-Project: + - PROJECT_ID + X-Request-Id: + - req_098e4aaa1133481fb16e82940c2d1558 + Openai-Processing-Ms: + - '11336' + X-Envoy-Upstream-Service-Time: + - '11339' + Cf-Cache-Status: + - DYNAMIC + Set-Cookie: + - __cf_bm=VjjuHDR.PNUCn8rM14xq7LsIIVAUuX1qazRUHp7vdKo-1763587143-1.0.1.1-6rMSTzbv6J_dtUYCiXPVoNUnqfa504.fh1huG7a8Z9qbhBG6ZBWlJpJ_osIb5hk9tpEVzq_wsR2wBTOqFGGQRaNXhB4Pcelr8f7oy8MPkOY; + path=/; expires=Wed, 19-Nov-25 21:49:03 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=l5FXBHKyzefvMOtNNobFrdBv5jST10aXOjtIkKwmAJ8-1763587143496-0.0.1.1-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + X-Content-Type-Options: + - nosniff + Server: + - cloudflare + Cf-Ray: + - 9a12be17982a1986-SJC + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: |- + { + "id": "resp_06eb76a2b12d8f5200691e343c273c819b89b7c2e558d3d3df", + "object": "response", + "created_at": 1763587132, + "status": "completed", + "background": false, + "billing": { + "payer": "developer" + }, + "error": null, + "incomplete_details": null, + "instructions": null, + "max_output_tokens": null, + "max_tool_calls": null, + "model": "gpt-4o-2024-08-06", + "output": [ + { + "id": "mcpl_06eb76a2b12d8f5200691e343c72e8819b88e9e5b814a0a925", + "type": "mcp_list_tools", + "server_label": "cloudflare", + "tools": [ + { + "annotations": { + "read_only": false + }, + "description": "Get information about Cloudflare's MCP Demo Day. Use this tool if the user asks about Cloudflare's MCP demo day", + "input_schema": { + "type": "object", + "properties": {} + }, + "name": "mcp_demo_day_info" + } + ] + }, + { + "id": "mcpl_06eb76a2b12d8f5200691e343c738c819b8ff83ca0c45ebd6f", + "type": "mcp_list_tools", + "server_label": "github", + "tools": [ + { + "annotations": { + "read_only": false + }, + "description": "Add review comment to the requester's latest pending pull request review. A pending review needs to already exist to call this (check with the user if not sure).", + "input_schema": { + "properties": { + "body": { + "description": "The text of the review comment", + "type": "string" + }, + "line": { + "description": "The line of the blob in the pull request diff that the comment applies to. For multi-line comments, the last line of the range", + "type": "number" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "path": { + "description": "The relative path to the file that necessitates a comment", + "type": "string" + }, + "pullNumber": { + "description": "Pull request number", + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + }, + "side": { + "description": "The side of the diff to comment on. LEFT indicates the previous state, RIGHT indicates the new state", + "enum": [ + "LEFT", + "RIGHT" + ], + "type": "string" + }, + "startLine": { + "description": "For multi-line comments, the first line of the range that the comment applies to", + "type": "number" + }, + "startSide": { + "description": "For multi-line comments, the starting side of the diff that the comment applies to. LEFT indicates the previous state, RIGHT indicates the new state", + "enum": [ + "LEFT", + "RIGHT" + ], + "type": "string" + }, + "subjectType": { + "description": "The level at which the comment is targeted", + "enum": [ + "FILE", + "LINE" + ], + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "pullNumber", + "path", + "body", + "subjectType" + ], + "type": "object" + }, + "name": "add_comment_to_pending_review" + }, + { + "annotations": { + "read_only": false + }, + "description": "Add a comment to a specific issue in a GitHub repository. Use this tool to add comments to pull requests as well (in this case pass pull request number as issue_number), but only if user is not asking specifically to add review comments.", + "input_schema": { + "properties": { + "body": { + "description": "Comment content", + "type": "string" + }, + "issue_number": { + "description": "Issue number to comment on", + "type": "number" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "issue_number", + "body" + ], + "type": "object" + }, + "name": "add_issue_comment" + }, + { + "annotations": { + "read_only": false + }, + "description": "Assign Copilot to a specific issue in a GitHub repository.\n\nThis tool can help with the following outcomes:\n- a Pull Request created with source code changes to resolve the issue\n\n\nMore information can be found at:\n- https://docs.github.com/en/copilot/using-github-copilot/using-copilot-coding-agent-to-work-on-tasks/about-assigning-tasks-to-copilot\n", + "input_schema": { + "properties": { + "issueNumber": { + "description": "Issue number", + "type": "number" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "issueNumber" + ], + "type": "object" + }, + "name": "assign_copilot_to_issue" + }, + { + "annotations": { + "read_only": false + }, + "description": "Create a new branch in a GitHub repository", + "input_schema": { + "properties": { + "branch": { + "description": "Name for new branch", + "type": "string" + }, + "from_branch": { + "description": "Source branch (defaults to repo default)", + "type": "string" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "branch" + ], + "type": "object" + }, + "name": "create_branch" + }, + { + "annotations": { + "read_only": false + }, + "description": "Create or update a single file in a GitHub repository. If updating, you must provide the SHA of the file you want to update. Use this tool to create or update a file in a GitHub repository remotely; do not use it for local file operations.", + "input_schema": { + "properties": { + "branch": { + "description": "Branch to create/update the file in", + "type": "string" + }, + "content": { + "description": "Content of the file", + "type": "string" + }, + "message": { + "description": "Commit message", + "type": "string" + }, + "owner": { + "description": "Repository owner (username or organization)", + "type": "string" + }, + "path": { + "description": "Path where to create/update the file", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + }, + "sha": { + "description": "Required if updating an existing file. The blob SHA of the file being replaced.", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "path", + "content", + "message", + "branch" + ], + "type": "object" + }, + "name": "create_or_update_file" + }, + { + "annotations": { + "read_only": false + }, + "description": "Create a new pull request in a GitHub repository.", + "input_schema": { + "properties": { + "base": { + "description": "Branch to merge into", + "type": "string" + }, + "body": { + "description": "PR description", + "type": "string" + }, + "draft": { + "description": "Create as draft PR", + "type": "boolean" + }, + "head": { + "description": "Branch containing changes", + "type": "string" + }, + "maintainer_can_modify": { + "description": "Allow maintainer edits", + "type": "boolean" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + }, + "title": { + "description": "PR title", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "title", + "head", + "base" + ], + "type": "object" + }, + "name": "create_pull_request" + }, + { + "annotations": { + "read_only": false + }, + "description": "Create a new GitHub repository in your account or specified organization", + "input_schema": { + "properties": { + "autoInit": { + "description": "Initialize with README", + "type": "boolean" + }, + "description": { + "description": "Repository description", + "type": "string" + }, + "name": { + "description": "Repository name", + "type": "string" + }, + "organization": { + "description": "Organization to create the repository in (omit to create in your personal account)", + "type": "string" + }, + "private": { + "description": "Whether repo should be private", + "type": "boolean" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "name": "create_repository" + }, + { + "annotations": { + "read_only": false + }, + "description": "Delete a file from a GitHub repository", + "input_schema": { + "properties": { + "branch": { + "description": "Branch to delete the file from", + "type": "string" + }, + "message": { + "description": "Commit message", + "type": "string" + }, + "owner": { + "description": "Repository owner (username or organization)", + "type": "string" + }, + "path": { + "description": "Path to the file to delete", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "path", + "message", + "branch" + ], + "type": "object" + }, + "name": "delete_file" + }, + { + "annotations": { + "read_only": false + }, + "description": "Fork a GitHub repository to your account or specified organization", + "input_schema": { + "properties": { + "organization": { + "description": "Organization to fork to", + "type": "string" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo" + ], + "type": "object" + }, + "name": "fork_repository" + }, + { + "annotations": { + "read_only": true + }, + "description": "Get details for a commit from a GitHub repository", + "input_schema": { + "properties": { + "include_diff": { + "default": true, + "description": "Whether to include file diffs and stats in the response. Default is true.", + "type": "boolean" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "page": { + "description": "Page number for pagination (min 1)", + "minimum": 1, + "type": "number" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + }, + "sha": { + "description": "Commit SHA, branch name, or tag name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "sha" + ], + "type": "object" + }, + "name": "get_commit" + }, + { + "annotations": { + "read_only": true + }, + "description": "Get the contents of a file or directory from a GitHub repository", + "input_schema": { + "properties": { + "owner": { + "description": "Repository owner (username or organization)", + "type": "string" + }, + "path": { + "default": "/", + "description": "Path to file/directory (directories must end with a slash '/')", + "type": "string" + }, + "ref": { + "description": "Accepts optional git refs such as `refs/tags/{tag}`, `refs/heads/{branch}` or `refs/pull/{pr_number}/head`", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + }, + "sha": { + "description": "Accepts optional commit SHA. If specified, it will be used instead of ref", + "type": "string" + } + }, + "required": [ + "owner", + "repo" + ], + "type": "object" + }, + "name": "get_file_contents" + }, + { + "annotations": { + "read_only": true + }, + "description": "Get a specific label from a repository.", + "input_schema": { + "properties": { + "name": { + "description": "Label name.", + "type": "string" + }, + "owner": { + "description": "Repository owner (username or organization name)", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "name" + ], + "type": "object" + }, + "name": "get_label" + }, + { + "annotations": { + "read_only": true + }, + "description": "Get the latest release in a GitHub repository", + "input_schema": { + "properties": { + "owner": { + "description": "Repository owner", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo" + ], + "type": "object" + }, + "name": "get_latest_release" + }, + { + "annotations": { + "read_only": true + }, + "description": "Get details of the authenticated GitHub user. Use this when a request is about the user's own profile for GitHub. Or when information is missing to build other tool calls.", + "input_schema": { + "properties": {}, + "type": "object" + }, + "name": "get_me" + }, + { + "annotations": { + "read_only": true + }, + "description": "Get a specific release by its tag name in a GitHub repository", + "input_schema": { + "properties": { + "owner": { + "description": "Repository owner", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + }, + "tag": { + "description": "Tag name (e.g., 'v1.0.0')", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "tag" + ], + "type": "object" + }, + "name": "get_release_by_tag" + }, + { + "annotations": { + "read_only": true + }, + "description": "Get details about a specific git tag in a GitHub repository", + "input_schema": { + "properties": { + "owner": { + "description": "Repository owner", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + }, + "tag": { + "description": "Tag name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "tag" + ], + "type": "object" + }, + "name": "get_tag" + }, + { + "annotations": { + "read_only": true + }, + "description": "Get member usernames of a specific team in an organization. Limited to organizations accessible with current credentials", + "input_schema": { + "properties": { + "org": { + "description": "Organization login (owner) that contains the team.", + "type": "string" + }, + "team_slug": { + "description": "Team slug", + "type": "string" + } + }, + "required": [ + "org", + "team_slug" + ], + "type": "object" + }, + "name": "get_team_members" + }, + { + "annotations": { + "read_only": true + }, + "description": "Get details of the teams the user is a member of. Limited to organizations accessible with current credentials", + "input_schema": { + "properties": { + "user": { + "description": "Username to get teams for. If not provided, uses the authenticated user.", + "type": "string" + } + }, + "type": "object" + }, + "name": "get_teams" + }, + { + "annotations": { + "read_only": true + }, + "description": "Get information about a specific issue in a GitHub repository.", + "input_schema": { + "properties": { + "issue_number": { + "description": "The number of the issue", + "type": "number" + }, + "method": { + "description": "The read operation to perform on a single issue. \nOptions are: \n1. get - Get details of a specific issue.\n2. get_comments - Get issue comments.\n3. get_sub_issues - Get sub-issues of the issue.\n4. get_labels - Get labels assigned to the issue.\n", + "enum": [ + "get", + "get_comments", + "get_sub_issues", + "get_labels" + ], + "type": "string" + }, + "owner": { + "description": "The owner of the repository", + "type": "string" + }, + "page": { + "description": "Page number for pagination (min 1)", + "minimum": 1, + "type": "number" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "repo": { + "description": "The name of the repository", + "type": "string" + } + }, + "required": [ + "method", + "owner", + "repo", + "issue_number" + ], + "type": "object" + }, + "name": "issue_read" + }, + { + "annotations": { + "read_only": false + }, + "description": "Create a new or update an existing issue in a GitHub repository.", + "input_schema": { + "properties": { + "assignees": { + "description": "Usernames to assign to this issue", + "items": { + "type": "string" + }, + "type": "array" + }, + "body": { + "description": "Issue body content", + "type": "string" + }, + "duplicate_of": { + "description": "Issue number that this issue is a duplicate of. Only used when state_reason is 'duplicate'.", + "type": "number" + }, + "issue_number": { + "description": "Issue number to update", + "type": "number" + }, + "labels": { + "description": "Labels to apply to this issue", + "items": { + "type": "string" + }, + "type": "array" + }, + "method": { + "description": "Write operation to perform on a single issue.\nOptions are: \n- 'create' - creates a new issue. \n- 'update' - updates an existing issue.\n", + "enum": [ + "create", + "update" + ], + "type": "string" + }, + "milestone": { + "description": "Milestone number", + "type": "number" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + }, + "state": { + "description": "New state", + "enum": [ + "open", + "closed" + ], + "type": "string" + }, + "state_reason": { + "description": "Reason for the state change. Ignored unless state is changed.", + "enum": [ + "completed", + "not_planned", + "duplicate" + ], + "type": "string" + }, + "title": { + "description": "Issue title", + "type": "string" + }, + "type": { + "description": "Type of this issue. Only use if the repository has issue types configured. Use list_issue_types tool to get valid type values for the organization. If the repository doesn't support issue types, omit this parameter.", + "type": "string" + } + }, + "required": [ + "method", + "owner", + "repo" + ], + "type": "object" + }, + "name": "issue_write" + }, + { + "annotations": { + "read_only": true + }, + "description": "List branches in a GitHub repository", + "input_schema": { + "properties": { + "owner": { + "description": "Repository owner", + "type": "string" + }, + "page": { + "description": "Page number for pagination (min 1)", + "minimum": 1, + "type": "number" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo" + ], + "type": "object" + }, + "name": "list_branches" + }, + { + "annotations": { + "read_only": true + }, + "description": "Get list of commits of a branch in a GitHub repository. Returns at least 30 results per page by default, but can return more if specified using the perPage parameter (up to 100).", + "input_schema": { + "properties": { + "author": { + "description": "Author username or email address to filter commits by", + "type": "string" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "page": { + "description": "Page number for pagination (min 1)", + "minimum": 1, + "type": "number" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + }, + "sha": { + "description": "Commit SHA, branch or tag name to list commits of. If not provided, uses the default branch of the repository. If a commit SHA is provided, will list commits up to that SHA.", + "type": "string" + } + }, + "required": [ + "owner", + "repo" + ], + "type": "object" + }, + "name": "list_commits" + }, + { + "annotations": { + "read_only": true + }, + "description": "List supported issue types for repository owner (organization).", + "input_schema": { + "properties": { + "owner": { + "description": "The organization owner of the repository", + "type": "string" + } + }, + "required": [ + "owner" + ], + "type": "object" + }, + "name": "list_issue_types" + }, + { + "annotations": { + "read_only": true + }, + "description": "List issues in a GitHub repository. For pagination, use the 'endCursor' from the previous response's 'pageInfo' in the 'after' parameter.", + "input_schema": { + "properties": { + "after": { + "description": "Cursor for pagination. Use the endCursor from the previous page's PageInfo for GraphQL APIs.", + "type": "string" + }, + "direction": { + "description": "Order direction. If provided, the 'orderBy' also needs to be provided.", + "enum": [ + "ASC", + "DESC" + ], + "type": "string" + }, + "labels": { + "description": "Filter by labels", + "items": { + "type": "string" + }, + "type": "array" + }, + "orderBy": { + "description": "Order issues by field. If provided, the 'direction' also needs to be provided.", + "enum": [ + "CREATED_AT", + "UPDATED_AT", + "COMMENTS" + ], + "type": "string" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + }, + "since": { + "description": "Filter by date (ISO 8601 timestamp)", + "type": "string" + }, + "state": { + "description": "Filter by state, by default both open and closed issues are returned when not provided", + "enum": [ + "OPEN", + "CLOSED" + ], + "type": "string" + } + }, + "required": [ + "owner", + "repo" + ], + "type": "object" + }, + "name": "list_issues" + }, + { + "annotations": { + "read_only": true + }, + "description": "List pull requests in a GitHub repository. If the user specifies an author, then DO NOT use this tool and use the search_pull_requests tool instead.", + "input_schema": { + "properties": { + "base": { + "description": "Filter by base branch", + "type": "string" + }, + "direction": { + "description": "Sort direction", + "enum": [ + "asc", + "desc" + ], + "type": "string" + }, + "head": { + "description": "Filter by head user/org and branch", + "type": "string" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "page": { + "description": "Page number for pagination (min 1)", + "minimum": 1, + "type": "number" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + }, + "sort": { + "description": "Sort by", + "enum": [ + "created", + "updated", + "popularity", + "long-running" + ], + "type": "string" + }, + "state": { + "description": "Filter by state", + "enum": [ + "open", + "closed", + "all" + ], + "type": "string" + } + }, + "required": [ + "owner", + "repo" + ], + "type": "object" + }, + "name": "list_pull_requests" + }, + { + "annotations": { + "read_only": true + }, + "description": "List releases in a GitHub repository", + "input_schema": { + "properties": { + "owner": { + "description": "Repository owner", + "type": "string" + }, + "page": { + "description": "Page number for pagination (min 1)", + "minimum": 1, + "type": "number" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo" + ], + "type": "object" + }, + "name": "list_releases" + }, + { + "annotations": { + "read_only": true + }, + "description": "List git tags in a GitHub repository", + "input_schema": { + "properties": { + "owner": { + "description": "Repository owner", + "type": "string" + }, + "page": { + "description": "Page number for pagination (min 1)", + "minimum": 1, + "type": "number" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo" + ], + "type": "object" + }, + "name": "list_tags" + }, + { + "annotations": { + "read_only": false + }, + "description": "Merge a pull request in a GitHub repository.", + "input_schema": { + "properties": { + "commit_message": { + "description": "Extra detail for merge commit", + "type": "string" + }, + "commit_title": { + "description": "Title for merge commit", + "type": "string" + }, + "merge_method": { + "description": "Merge method", + "enum": [ + "merge", + "squash", + "rebase" + ], + "type": "string" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "pullNumber": { + "description": "Pull request number", + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "pullNumber" + ], + "type": "object" + }, + "name": "merge_pull_request" + }, + { + "annotations": { + "read_only": true + }, + "description": "Get information on a specific pull request in GitHub repository.", + "input_schema": { + "properties": { + "method": { + "description": "Action to specify what pull request data needs to be retrieved from GitHub. \nPossible options: \n 1. get - Get details of a specific pull request.\n 2. get_diff - Get the diff of a pull request.\n 3. get_status - Get status of a head commit in a pull request. This reflects status of builds and checks.\n 4. get_files - Get the list of files changed in a pull request. Use with pagination parameters to control the number of results returned.\n 5. get_review_comments - Get the review comments on a pull request. They are comments made on a portion of the unified diff during a pull request review. Use with pagination parameters to control the number of results returned.\n 6. get_reviews - Get the reviews on a pull request. When asked for review comments, use get_review_comments method.\n 7. get_comments - Get comments on a pull request. Use this if user doesn't specifically want review comments. Use with pagination parameters to control the number of results returned.\n", + "enum": [ + "get", + "get_diff", + "get_status", + "get_files", + "get_review_comments", + "get_reviews", + "get_comments" + ], + "type": "string" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "page": { + "description": "Page number for pagination (min 1)", + "minimum": 1, + "type": "number" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "pullNumber": { + "description": "Pull request number", + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "method", + "owner", + "repo", + "pullNumber" + ], + "type": "object" + }, + "name": "pull_request_read" + }, + { + "annotations": { + "read_only": false + }, + "description": "Create and/or submit, delete review of a pull request.\n\nAvailable methods:\n- create: Create a new review of a pull request. If \"event\" parameter is provided, the review is submitted. If \"event\" is omitted, a pending review is created.\n- submit_pending: Submit an existing pending review of a pull request. This requires that a pending review exists for the current user on the specified pull request. The \"body\" and \"event\" parameters are used when submitting the review.\n- delete_pending: Delete an existing pending review of a pull request. This requires that a pending review exists for the current user on the specified pull request.\n", + "input_schema": { + "properties": { + "body": { + "description": "Review comment text", + "type": "string" + }, + "commitID": { + "description": "SHA of commit to review", + "type": "string" + }, + "event": { + "description": "Review action to perform.", + "enum": [ + "APPROVE", + "REQUEST_CHANGES", + "COMMENT" + ], + "type": "string" + }, + "method": { + "description": "The write operation to perform on pull request review.", + "enum": [ + "create", + "submit_pending", + "delete_pending" + ], + "type": "string" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "pullNumber": { + "description": "Pull request number", + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "method", + "owner", + "repo", + "pullNumber" + ], + "type": "object" + }, + "name": "pull_request_review_write" + }, + { + "annotations": { + "read_only": false + }, + "description": "Push multiple files to a GitHub repository in a single commit", + "input_schema": { + "properties": { + "branch": { + "description": "Branch to push to", + "type": "string" + }, + "files": { + "description": "Array of file objects to push, each object with path (string) and content (string)", + "items": { + "additionalProperties": false, + "properties": { + "content": { + "description": "file content", + "type": "string" + }, + "path": { + "description": "path to the file", + "type": "string" + } + }, + "required": [ + "path", + "content" + ], + "type": "object" + }, + "type": "array" + }, + "message": { + "description": "Commit message", + "type": "string" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "branch", + "files", + "message" + ], + "type": "object" + }, + "name": "push_files" + }, + { + "annotations": { + "read_only": false + }, + "description": "Request a GitHub Copilot code review for a pull request. Use this for automated feedback on pull requests, usually before requesting a human reviewer.", + "input_schema": { + "properties": { + "owner": { + "description": "Repository owner", + "type": "string" + }, + "pullNumber": { + "description": "Pull request number", + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "pullNumber" + ], + "type": "object" + }, + "name": "request_copilot_review" + }, + { + "annotations": { + "read_only": true + }, + "description": "Fast and precise code search across ALL GitHub repositories using GitHub's native search engine. Best for finding exact symbols, functions, classes, or specific code patterns.", + "input_schema": { + "properties": { + "order": { + "description": "Sort order for results", + "enum": [ + "asc", + "desc" + ], + "type": "string" + }, + "page": { + "description": "Page number for pagination (min 1)", + "minimum": 1, + "type": "number" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "query": { + "description": "Search query using GitHub's powerful code search syntax. Examples: 'content:Skill language:Java org:github', 'NOT is:archived language:Python OR language:go', 'repo:github/github-mcp-server'. Supports exact matching, language filters, path filters, and more.", + "type": "string" + }, + "sort": { + "description": "Sort field ('indexed' only)", + "type": "string" + } + }, + "required": [ + "query" + ], + "type": "object" + }, + "name": "search_code" + }, + { + "annotations": { + "read_only": true + }, + "description": "Search for issues in GitHub repositories using issues search syntax already scoped to is:issue", + "input_schema": { + "properties": { + "order": { + "description": "Sort order", + "enum": [ + "asc", + "desc" + ], + "type": "string" + }, + "owner": { + "description": "Optional repository owner. If provided with repo, only issues for this repository are listed.", + "type": "string" + }, + "page": { + "description": "Page number for pagination (min 1)", + "minimum": 1, + "type": "number" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "query": { + "description": "Search query using GitHub issues search syntax", + "type": "string" + }, + "repo": { + "description": "Optional repository name. If provided with owner, only issues for this repository are listed.", + "type": "string" + }, + "sort": { + "description": "Sort field by number of matches of categories, defaults to best match", + "enum": [ + "comments", + "reactions", + "reactions-+1", + "reactions--1", + "reactions-smile", + "reactions-thinking_face", + "reactions-heart", + "reactions-tada", + "interactions", + "created", + "updated" + ], + "type": "string" + } + }, + "required": [ + "query" + ], + "type": "object" + }, + "name": "search_issues" + }, + { + "annotations": { + "read_only": true + }, + "description": "Search for pull requests in GitHub repositories using issues search syntax already scoped to is:pr", + "input_schema": { + "properties": { + "order": { + "description": "Sort order", + "enum": [ + "asc", + "desc" + ], + "type": "string" + }, + "owner": { + "description": "Optional repository owner. If provided with repo, only pull requests for this repository are listed.", + "type": "string" + }, + "page": { + "description": "Page number for pagination (min 1)", + "minimum": 1, + "type": "number" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "query": { + "description": "Search query using GitHub pull request search syntax", + "type": "string" + }, + "repo": { + "description": "Optional repository name. If provided with owner, only pull requests for this repository are listed.", + "type": "string" + }, + "sort": { + "description": "Sort field by number of matches of categories, defaults to best match", + "enum": [ + "comments", + "reactions", + "reactions-+1", + "reactions--1", + "reactions-smile", + "reactions-thinking_face", + "reactions-heart", + "reactions-tada", + "interactions", + "created", + "updated" + ], + "type": "string" + } + }, + "required": [ + "query" + ], + "type": "object" + }, + "name": "search_pull_requests" + }, + { + "annotations": { + "read_only": true + }, + "description": "Find GitHub repositories by name, description, readme, topics, or other metadata. Perfect for discovering projects, finding examples, or locating specific repositories across GitHub.", + "input_schema": { + "properties": { + "minimal_output": { + "default": true, + "description": "Return minimal repository information (default: true). When false, returns full GitHub API repository objects.", + "type": "boolean" + }, + "order": { + "description": "Sort order", + "enum": [ + "asc", + "desc" + ], + "type": "string" + }, + "page": { + "description": "Page number for pagination (min 1)", + "minimum": 1, + "type": "number" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "query": { + "description": "Repository search query. Examples: 'machine learning in:name stars:>1000 language:python', 'topic:react', 'user:facebook'. Supports advanced search syntax for precise filtering.", + "type": "string" + }, + "sort": { + "description": "Sort repositories by field, defaults to best match", + "enum": [ + "stars", + "forks", + "help-wanted-issues", + "updated" + ], + "type": "string" + } + }, + "required": [ + "query" + ], + "type": "object" + }, + "name": "search_repositories" + }, + { + "annotations": { + "read_only": true + }, + "description": "Find GitHub users by username, real name, or other profile information. Useful for locating developers, contributors, or team members.", + "input_schema": { + "properties": { + "order": { + "description": "Sort order", + "enum": [ + "asc", + "desc" + ], + "type": "string" + }, + "page": { + "description": "Page number for pagination (min 1)", + "minimum": 1, + "type": "number" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "query": { + "description": "User search query. Examples: 'john smith', 'location:seattle', 'followers:>100'. Search is automatically scoped to type:user.", + "type": "string" + }, + "sort": { + "description": "Sort users by number of followers or repositories, or when the person joined GitHub.", + "enum": [ + "followers", + "repositories", + "joined" + ], + "type": "string" + } + }, + "required": [ + "query" + ], + "type": "object" + }, + "name": "search_users" + }, + { + "annotations": { + "read_only": false + }, + "description": "Add a sub-issue to a parent issue in a GitHub repository.", + "input_schema": { + "properties": { + "after_id": { + "description": "The ID of the sub-issue to be prioritized after (either after_id OR before_id should be specified)", + "type": "number" + }, + "before_id": { + "description": "The ID of the sub-issue to be prioritized before (either after_id OR before_id should be specified)", + "type": "number" + }, + "issue_number": { + "description": "The number of the parent issue", + "type": "number" + }, + "method": { + "description": "The action to perform on a single sub-issue\nOptions are:\n- 'add' - add a sub-issue to a parent issue in a GitHub repository.\n- 'remove' - remove a sub-issue from a parent issue in a GitHub repository.\n- 'reprioritize' - change the order of sub-issues within a parent issue in a GitHub repository. Use either 'after_id' or 'before_id' to specify the new position.\n\t\t\t\t", + "type": "string" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "replace_parent": { + "description": "When true, replaces the sub-issue's current parent issue. Use with 'add' method only.", + "type": "boolean" + }, + "repo": { + "description": "Repository name", + "type": "string" + }, + "sub_issue_id": { + "description": "The ID of the sub-issue to add. ID is not the same as issue number", + "type": "number" + } + }, + "required": [ + "method", + "owner", + "repo", + "issue_number", + "sub_issue_id" + ], + "type": "object" + }, + "name": "sub_issue_write" + }, + { + "annotations": { + "read_only": false + }, + "description": "Update an existing pull request in a GitHub repository.", + "input_schema": { + "properties": { + "base": { + "description": "New base branch name", + "type": "string" + }, + "body": { + "description": "New description", + "type": "string" + }, + "draft": { + "description": "Mark pull request as draft (true) or ready for review (false)", + "type": "boolean" + }, + "maintainer_can_modify": { + "description": "Allow maintainer edits", + "type": "boolean" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "pullNumber": { + "description": "Pull request number to update", + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + }, + "reviewers": { + "description": "GitHub usernames to request reviews from", + "items": { + "type": "string" + }, + "type": "array" + }, + "state": { + "description": "New state", + "enum": [ + "open", + "closed" + ], + "type": "string" + }, + "title": { + "description": "New title", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "pullNumber" + ], + "type": "object" + }, + "name": "update_pull_request" + }, + { + "annotations": { + "read_only": false + }, + "description": "Update the branch of a pull request with the latest changes from the base branch.", + "input_schema": { + "properties": { + "expectedHeadSha": { + "description": "The expected SHA of the pull request's HEAD ref", + "type": "string" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "pullNumber": { + "description": "Pull request number", + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "pullNumber" + ], + "type": "object" + }, + "name": "update_pull_request_branch" + } + ] + }, + { + "id": "msg_06eb76a2b12d8f5200691e343fd2e4819bb70ba9c33446de71", + "type": "message", + "status": "completed", + "content": [ + { + "type": "output_text", + "annotations": [], + "logprobs": [], + "text": "Here are some of the latest developments in AI:\n\n1. **Advancements in Generative AI**: Techniques like Generative Adversarial Networks (GANs) and diffusion models are producing high-quality images, videos, and audio. Applications in content creation and digital art are expanding rapidly.\n\n2. **AI in Healthcare**: AI is revolutionizing diagnostics, drug discovery, and personalized medicine. Machine learning models are aiding in the early detection of diseases and optimizing treatment plans.\n\n3. **Natural Language Processing (NLP)**: Large language models, such as OpenAI's GPT-3 and GPT-4, continue to advance, improving the ability of machines to understand and generate human-like text. This is impacting customer service, content generation, and more.\n\n4. **AI Ethics and Regulation**: As AI systems become more widespread, there is a growing focus on ethical considerations, including bias, privacy, and transparency. Governments and organizations are working on developing regulations and guidelines.\n\n5. **AI in Autonomous Vehicles**: AI is being integrated into self-driving cars, drones, and other autonomous systems, enhancing navigation, safety, and efficiency.\n\n6. **AI and Quantum Computing**: Research is being conducted on leveraging quantum computing for AI to solve complex problems more efficiently than classical computers.\n\n7. **Computer Vision**: AI models are becoming better at image and video recognition, with applications ranging from security and surveillance to agriculture and industrial inspection.\n\n8. **AI in Finance**: AI is increasingly used for fraud detection, personalized banking, and algorithmic trading, optimizing various financial services.\n\nThese developments represent just a snapshot of the rapidly evolving AI landscape, which continues to impact numerous industries and daily life." + } + ], + "role": "assistant" + } + ], + "parallel_tool_calls": true, + "previous_response_id": null, + "prompt_cache_key": null, + "prompt_cache_retention": null, + "reasoning": { + "effort": null, + "summary": null + }, + "safety_identifier": null, + "service_tier": "default", + "store": true, + "temperature": 1.0, + "text": { + "format": { + "type": "text" + }, + "verbosity": "medium" + }, + "tool_choice": "auto", + "tools": [ + { + "type": "mcp", + "allowed_tools": null, + "headers": null, + "require_approval": "always", + "server_description": null, + "server_label": "cloudflare", + "server_url": "https://demo-day.mcp.cloudflare.com/sse" + }, + { + "type": "mcp", + "allowed_tools": null, + "authorization": "", + "headers": null, + "require_approval": "always", + "server_description": null, + "server_label": "github", + "server_url": "https://api.githubcopilot.com/mcp/" + } + ], + "top_logprobs": 0, + "top_p": 1.0, + "truncation": "disabled", + "usage": { + "input_tokens": 5293, + "input_tokens_details": { + "cached_tokens": 0 + }, + "output_tokens": 343, + "output_tokens_details": { + "reasoning_tokens": 0 + }, + "total_tokens": 5636 + }, + "user": null, + "metadata": {} + } + recorded_at: Wed, 19 Nov 2025 21:19:03 GMT +recorded_with: VCR 6.3.1 diff --git a/test/fixtures/vcr_cassettes/docs/actions/mcps/native_format_anthropic.yml b/test/fixtures/vcr_cassettes/docs/actions/mcps/native_format_anthropic.yml new file mode 100644 index 00000000..2d0bad85 --- /dev/null +++ b/test/fixtures/vcr_cassettes/docs/actions/mcps/native_format_anthropic.yml @@ -0,0 +1,109 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.anthropic.com/v1/messages?beta=true + body: + encoding: UTF-8 + string: '{"model":"claude-haiku-4-5","messages":[{"content":"What can you do?","role":"user"}],"mcp_servers":[{"name":"cloudflare","type":"url","url":"https://demo-day.mcp.cloudflare.com/sse"}],"max_tokens":4096}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json + User-Agent: + - Anthropic::Client/Ruby 1.14.0 + Host: + - api.anthropic.com + X-Stainless-Arch: + - arm64 + X-Stainless-Lang: + - ruby + X-Stainless-Os: + - MacOS + X-Stainless-Package-Version: + - 1.14.0 + X-Stainless-Runtime: + - ruby + X-Stainless-Runtime-Version: + - 3.4.7 + Content-Type: + - application/json + Anthropic-Version: + - '2023-06-01' + X-Api-Key: + - ACCESS_TOKEN + Anthropic-Beta: + - mcp-client-2025-04-04 + X-Stainless-Retry-Count: + - '0' + X-Stainless-Timeout: + - '600.0' + Content-Length: + - '203' + response: + status: + code: 200 + message: OK + headers: + Date: + - Wed, 19 Nov 2025 21:18:48 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Anthropic-Ratelimit-Input-Tokens-Limit: + - '4000000' + Anthropic-Ratelimit-Input-Tokens-Remaining: + - '4000000' + Anthropic-Ratelimit-Input-Tokens-Reset: + - '2025-11-19T21:18:47Z' + Anthropic-Ratelimit-Output-Tokens-Limit: + - '800000' + Anthropic-Ratelimit-Output-Tokens-Remaining: + - '800000' + Anthropic-Ratelimit-Output-Tokens-Reset: + - '2025-11-19T21:18:48Z' + Anthropic-Ratelimit-Requests-Limit: + - '4000' + Anthropic-Ratelimit-Requests-Remaining: + - '3999' + Anthropic-Ratelimit-Requests-Reset: + - '2025-11-19T21:18:47Z' + Retry-After: + - '13' + Anthropic-Ratelimit-Tokens-Limit: + - '4800000' + Anthropic-Ratelimit-Tokens-Remaining: + - '4800000' + Anthropic-Ratelimit-Tokens-Reset: + - '2025-11-19T21:18:47Z' + Request-Id: + - req_011CVHwnXBEM3NrXrHRmWjgL + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Anthropic-Organization-Id: + - 2557c2f2-bcfa-4054-9fa8-ae13b3b47d6b + X-Envoy-Upstream-Service-Time: + - '3401' + Cf-Cache-Status: + - DYNAMIC + X-Robots-Tag: + - none + Server: + - cloudflare + Cf-Ray: + - 9a12bdec7c19270c-SJC + body: + encoding: ASCII-8BIT + string: '{"model":"claude-haiku-4-5-20251001","id":"msg_011Hz8oWUNyrUP8CRsmtn8eM","type":"message","role":"assistant","content":[{"type":"text","text":"I + can help you with information about **Cloudflare''s MCP Demo Day**! \n\nIf + you have any questions about:\n- What Cloudflare''s MCP Demo Day is\n- When + and where it''s happening\n- What will be showcased\n- How to participate + or attend\n- Any other details about the event\n\nJust ask me, and I''ll retrieve + the information for you using the tools available to me.\n\nIs there anything + specific about Cloudflare''s MCP Demo Day you''d like to know?"}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":581,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":115,"service_tier":"standard"}}' + recorded_at: Wed, 19 Nov 2025 21:18:48 GMT +recorded_with: VCR 6.3.1 diff --git a/test/fixtures/vcr_cassettes/docs/actions/mcps/native_format_openai.yml b/test/fixtures/vcr_cassettes/docs/actions/mcps/native_format_openai.yml new file mode 100644 index 00000000..a5937502 --- /dev/null +++ b/test/fixtures/vcr_cassettes/docs/actions/mcps/native_format_openai.yml @@ -0,0 +1,1906 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.openai.com/v1/responses + body: + encoding: UTF-8 + string: '{"model":"gpt-4o","input":"What can you do?","tools":[{"type":"mcp","server_label":"github","server_url":"https://api.githubcopilot.com/mcp/","authorization":"GITHUB_MCP_TOKEN"}]}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json + User-Agent: + - OpenAI::Client/Ruby 0.35.1 + Host: + - api.openai.com + X-Stainless-Arch: + - arm64 + X-Stainless-Lang: + - ruby + X-Stainless-Os: + - MacOS + X-Stainless-Package-Version: + - 0.35.1 + X-Stainless-Runtime: + - ruby + X-Stainless-Runtime-Version: + - 3.4.7 + Content-Type: + - application/json + Openai-Organization: + - ORGANIZATION_ID + Openai-Project: + - PROJECT_ID + Authorization: + - Bearer ACCESS_TOKEN + X-Stainless-Retry-Count: + - '0' + X-Stainless-Timeout: + - '600.0' + Content-Length: + - '256' + response: + status: + code: 200 + message: OK + headers: + Date: + - Wed, 19 Nov 2025 21:18:45 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + X-Ratelimit-Limit-Requests: + - '10000' + X-Ratelimit-Limit-Tokens: + - '30000000' + X-Ratelimit-Remaining-Requests: + - '9999' + X-Ratelimit-Remaining-Tokens: + - '29994744' + X-Ratelimit-Reset-Requests: + - 6ms + X-Ratelimit-Reset-Tokens: + - 10ms + Openai-Version: + - '2020-10-01' + Openai-Organization: + - ORGANIZATION_ID + Openai-Project: + - PROJECT_ID + X-Request-Id: + - req_dc8a2eebab2444798f51ec876a94cf44 + Openai-Processing-Ms: + - '4537' + X-Envoy-Upstream-Service-Time: + - '4540' + Cf-Cache-Status: + - DYNAMIC + Set-Cookie: + - __cf_bm=c4iTaSV9bL2OmT1GObGllvGoG3URyXubGmh65fQXrKQ-1763587125-1.0.1.1-.Wo_oU2NLP3rae4sgEq5osjfYrd4gftWkXb2dkV59dirBuT4qHGiBWvpK9HrE.aMulBRA6t4q7CD_06WQHJjiwzmq7MDjVvJn6KZhdpG35Y; + path=/; expires=Wed, 19-Nov-25 21:48:45 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=4GMBp3YRCrAwye8TyVJeBl9eKm6TqRjdFn3M4LwZkLg-1763587125106-0.0.1.1-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + X-Content-Type-Options: + - nosniff + Server: + - cloudflare + Cf-Ray: + - 9a12bdcf39f43ad4-SJC + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: |- + { + "id": "resp_05c47f40bf062be000691e343092ac819bb56c281765935f2a", + "object": "response", + "created_at": 1763587120, + "status": "completed", + "background": false, + "billing": { + "payer": "developer" + }, + "error": null, + "incomplete_details": null, + "instructions": null, + "max_output_tokens": null, + "max_tool_calls": null, + "model": "gpt-4o-2024-08-06", + "output": [ + { + "id": "mcpl_05c47f40bf062be000691e3430c7d0819b96fe9b1a02fecabc", + "type": "mcp_list_tools", + "server_label": "github", + "tools": [ + { + "annotations": { + "read_only": false + }, + "description": "Add review comment to the requester's latest pending pull request review. A pending review needs to already exist to call this (check with the user if not sure).", + "input_schema": { + "properties": { + "body": { + "description": "The text of the review comment", + "type": "string" + }, + "line": { + "description": "The line of the blob in the pull request diff that the comment applies to. For multi-line comments, the last line of the range", + "type": "number" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "path": { + "description": "The relative path to the file that necessitates a comment", + "type": "string" + }, + "pullNumber": { + "description": "Pull request number", + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + }, + "side": { + "description": "The side of the diff to comment on. LEFT indicates the previous state, RIGHT indicates the new state", + "enum": [ + "LEFT", + "RIGHT" + ], + "type": "string" + }, + "startLine": { + "description": "For multi-line comments, the first line of the range that the comment applies to", + "type": "number" + }, + "startSide": { + "description": "For multi-line comments, the starting side of the diff that the comment applies to. LEFT indicates the previous state, RIGHT indicates the new state", + "enum": [ + "LEFT", + "RIGHT" + ], + "type": "string" + }, + "subjectType": { + "description": "The level at which the comment is targeted", + "enum": [ + "FILE", + "LINE" + ], + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "pullNumber", + "path", + "body", + "subjectType" + ], + "type": "object" + }, + "name": "add_comment_to_pending_review" + }, + { + "annotations": { + "read_only": false + }, + "description": "Add a comment to a specific issue in a GitHub repository. Use this tool to add comments to pull requests as well (in this case pass pull request number as issue_number), but only if user is not asking specifically to add review comments.", + "input_schema": { + "properties": { + "body": { + "description": "Comment content", + "type": "string" + }, + "issue_number": { + "description": "Issue number to comment on", + "type": "number" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "issue_number", + "body" + ], + "type": "object" + }, + "name": "add_issue_comment" + }, + { + "annotations": { + "read_only": false + }, + "description": "Assign Copilot to a specific issue in a GitHub repository.\n\nThis tool can help with the following outcomes:\n- a Pull Request created with source code changes to resolve the issue\n\n\nMore information can be found at:\n- https://docs.github.com/en/copilot/using-github-copilot/using-copilot-coding-agent-to-work-on-tasks/about-assigning-tasks-to-copilot\n", + "input_schema": { + "properties": { + "issueNumber": { + "description": "Issue number", + "type": "number" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "issueNumber" + ], + "type": "object" + }, + "name": "assign_copilot_to_issue" + }, + { + "annotations": { + "read_only": false + }, + "description": "Create a new branch in a GitHub repository", + "input_schema": { + "properties": { + "branch": { + "description": "Name for new branch", + "type": "string" + }, + "from_branch": { + "description": "Source branch (defaults to repo default)", + "type": "string" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "branch" + ], + "type": "object" + }, + "name": "create_branch" + }, + { + "annotations": { + "read_only": false + }, + "description": "Create or update a single file in a GitHub repository. If updating, you must provide the SHA of the file you want to update. Use this tool to create or update a file in a GitHub repository remotely; do not use it for local file operations.", + "input_schema": { + "properties": { + "branch": { + "description": "Branch to create/update the file in", + "type": "string" + }, + "content": { + "description": "Content of the file", + "type": "string" + }, + "message": { + "description": "Commit message", + "type": "string" + }, + "owner": { + "description": "Repository owner (username or organization)", + "type": "string" + }, + "path": { + "description": "Path where to create/update the file", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + }, + "sha": { + "description": "Required if updating an existing file. The blob SHA of the file being replaced.", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "path", + "content", + "message", + "branch" + ], + "type": "object" + }, + "name": "create_or_update_file" + }, + { + "annotations": { + "read_only": false + }, + "description": "Create a new pull request in a GitHub repository.", + "input_schema": { + "properties": { + "base": { + "description": "Branch to merge into", + "type": "string" + }, + "body": { + "description": "PR description", + "type": "string" + }, + "draft": { + "description": "Create as draft PR", + "type": "boolean" + }, + "head": { + "description": "Branch containing changes", + "type": "string" + }, + "maintainer_can_modify": { + "description": "Allow maintainer edits", + "type": "boolean" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + }, + "title": { + "description": "PR title", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "title", + "head", + "base" + ], + "type": "object" + }, + "name": "create_pull_request" + }, + { + "annotations": { + "read_only": false + }, + "description": "Create a new GitHub repository in your account or specified organization", + "input_schema": { + "properties": { + "autoInit": { + "description": "Initialize with README", + "type": "boolean" + }, + "description": { + "description": "Repository description", + "type": "string" + }, + "name": { + "description": "Repository name", + "type": "string" + }, + "organization": { + "description": "Organization to create the repository in (omit to create in your personal account)", + "type": "string" + }, + "private": { + "description": "Whether repo should be private", + "type": "boolean" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "name": "create_repository" + }, + { + "annotations": { + "read_only": false + }, + "description": "Delete a file from a GitHub repository", + "input_schema": { + "properties": { + "branch": { + "description": "Branch to delete the file from", + "type": "string" + }, + "message": { + "description": "Commit message", + "type": "string" + }, + "owner": { + "description": "Repository owner (username or organization)", + "type": "string" + }, + "path": { + "description": "Path to the file to delete", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "path", + "message", + "branch" + ], + "type": "object" + }, + "name": "delete_file" + }, + { + "annotations": { + "read_only": false + }, + "description": "Fork a GitHub repository to your account or specified organization", + "input_schema": { + "properties": { + "organization": { + "description": "Organization to fork to", + "type": "string" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo" + ], + "type": "object" + }, + "name": "fork_repository" + }, + { + "annotations": { + "read_only": true + }, + "description": "Get details for a commit from a GitHub repository", + "input_schema": { + "properties": { + "include_diff": { + "default": true, + "description": "Whether to include file diffs and stats in the response. Default is true.", + "type": "boolean" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "page": { + "description": "Page number for pagination (min 1)", + "minimum": 1, + "type": "number" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + }, + "sha": { + "description": "Commit SHA, branch name, or tag name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "sha" + ], + "type": "object" + }, + "name": "get_commit" + }, + { + "annotations": { + "read_only": true + }, + "description": "Get the contents of a file or directory from a GitHub repository", + "input_schema": { + "properties": { + "owner": { + "description": "Repository owner (username or organization)", + "type": "string" + }, + "path": { + "default": "/", + "description": "Path to file/directory (directories must end with a slash '/')", + "type": "string" + }, + "ref": { + "description": "Accepts optional git refs such as `refs/tags/{tag}`, `refs/heads/{branch}` or `refs/pull/{pr_number}/head`", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + }, + "sha": { + "description": "Accepts optional commit SHA. If specified, it will be used instead of ref", + "type": "string" + } + }, + "required": [ + "owner", + "repo" + ], + "type": "object" + }, + "name": "get_file_contents" + }, + { + "annotations": { + "read_only": true + }, + "description": "Get a specific label from a repository.", + "input_schema": { + "properties": { + "name": { + "description": "Label name.", + "type": "string" + }, + "owner": { + "description": "Repository owner (username or organization name)", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "name" + ], + "type": "object" + }, + "name": "get_label" + }, + { + "annotations": { + "read_only": true + }, + "description": "Get the latest release in a GitHub repository", + "input_schema": { + "properties": { + "owner": { + "description": "Repository owner", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo" + ], + "type": "object" + }, + "name": "get_latest_release" + }, + { + "annotations": { + "read_only": true + }, + "description": "Get details of the authenticated GitHub user. Use this when a request is about the user's own profile for GitHub. Or when information is missing to build other tool calls.", + "input_schema": { + "properties": {}, + "type": "object" + }, + "name": "get_me" + }, + { + "annotations": { + "read_only": true + }, + "description": "Get a specific release by its tag name in a GitHub repository", + "input_schema": { + "properties": { + "owner": { + "description": "Repository owner", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + }, + "tag": { + "description": "Tag name (e.g., 'v1.0.0')", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "tag" + ], + "type": "object" + }, + "name": "get_release_by_tag" + }, + { + "annotations": { + "read_only": true + }, + "description": "Get details about a specific git tag in a GitHub repository", + "input_schema": { + "properties": { + "owner": { + "description": "Repository owner", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + }, + "tag": { + "description": "Tag name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "tag" + ], + "type": "object" + }, + "name": "get_tag" + }, + { + "annotations": { + "read_only": true + }, + "description": "Get member usernames of a specific team in an organization. Limited to organizations accessible with current credentials", + "input_schema": { + "properties": { + "org": { + "description": "Organization login (owner) that contains the team.", + "type": "string" + }, + "team_slug": { + "description": "Team slug", + "type": "string" + } + }, + "required": [ + "org", + "team_slug" + ], + "type": "object" + }, + "name": "get_team_members" + }, + { + "annotations": { + "read_only": true + }, + "description": "Get details of the teams the user is a member of. Limited to organizations accessible with current credentials", + "input_schema": { + "properties": { + "user": { + "description": "Username to get teams for. If not provided, uses the authenticated user.", + "type": "string" + } + }, + "type": "object" + }, + "name": "get_teams" + }, + { + "annotations": { + "read_only": true + }, + "description": "Get information about a specific issue in a GitHub repository.", + "input_schema": { + "properties": { + "issue_number": { + "description": "The number of the issue", + "type": "number" + }, + "method": { + "description": "The read operation to perform on a single issue. \nOptions are: \n1. get - Get details of a specific issue.\n2. get_comments - Get issue comments.\n3. get_sub_issues - Get sub-issues of the issue.\n4. get_labels - Get labels assigned to the issue.\n", + "enum": [ + "get", + "get_comments", + "get_sub_issues", + "get_labels" + ], + "type": "string" + }, + "owner": { + "description": "The owner of the repository", + "type": "string" + }, + "page": { + "description": "Page number for pagination (min 1)", + "minimum": 1, + "type": "number" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "repo": { + "description": "The name of the repository", + "type": "string" + } + }, + "required": [ + "method", + "owner", + "repo", + "issue_number" + ], + "type": "object" + }, + "name": "issue_read" + }, + { + "annotations": { + "read_only": false + }, + "description": "Create a new or update an existing issue in a GitHub repository.", + "input_schema": { + "properties": { + "assignees": { + "description": "Usernames to assign to this issue", + "items": { + "type": "string" + }, + "type": "array" + }, + "body": { + "description": "Issue body content", + "type": "string" + }, + "duplicate_of": { + "description": "Issue number that this issue is a duplicate of. Only used when state_reason is 'duplicate'.", + "type": "number" + }, + "issue_number": { + "description": "Issue number to update", + "type": "number" + }, + "labels": { + "description": "Labels to apply to this issue", + "items": { + "type": "string" + }, + "type": "array" + }, + "method": { + "description": "Write operation to perform on a single issue.\nOptions are: \n- 'create' - creates a new issue. \n- 'update' - updates an existing issue.\n", + "enum": [ + "create", + "update" + ], + "type": "string" + }, + "milestone": { + "description": "Milestone number", + "type": "number" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + }, + "state": { + "description": "New state", + "enum": [ + "open", + "closed" + ], + "type": "string" + }, + "state_reason": { + "description": "Reason for the state change. Ignored unless state is changed.", + "enum": [ + "completed", + "not_planned", + "duplicate" + ], + "type": "string" + }, + "title": { + "description": "Issue title", + "type": "string" + }, + "type": { + "description": "Type of this issue. Only use if the repository has issue types configured. Use list_issue_types tool to get valid type values for the organization. If the repository doesn't support issue types, omit this parameter.", + "type": "string" + } + }, + "required": [ + "method", + "owner", + "repo" + ], + "type": "object" + }, + "name": "issue_write" + }, + { + "annotations": { + "read_only": true + }, + "description": "List branches in a GitHub repository", + "input_schema": { + "properties": { + "owner": { + "description": "Repository owner", + "type": "string" + }, + "page": { + "description": "Page number for pagination (min 1)", + "minimum": 1, + "type": "number" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo" + ], + "type": "object" + }, + "name": "list_branches" + }, + { + "annotations": { + "read_only": true + }, + "description": "Get list of commits of a branch in a GitHub repository. Returns at least 30 results per page by default, but can return more if specified using the perPage parameter (up to 100).", + "input_schema": { + "properties": { + "author": { + "description": "Author username or email address to filter commits by", + "type": "string" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "page": { + "description": "Page number for pagination (min 1)", + "minimum": 1, + "type": "number" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + }, + "sha": { + "description": "Commit SHA, branch or tag name to list commits of. If not provided, uses the default branch of the repository. If a commit SHA is provided, will list commits up to that SHA.", + "type": "string" + } + }, + "required": [ + "owner", + "repo" + ], + "type": "object" + }, + "name": "list_commits" + }, + { + "annotations": { + "read_only": true + }, + "description": "List supported issue types for repository owner (organization).", + "input_schema": { + "properties": { + "owner": { + "description": "The organization owner of the repository", + "type": "string" + } + }, + "required": [ + "owner" + ], + "type": "object" + }, + "name": "list_issue_types" + }, + { + "annotations": { + "read_only": true + }, + "description": "List issues in a GitHub repository. For pagination, use the 'endCursor' from the previous response's 'pageInfo' in the 'after' parameter.", + "input_schema": { + "properties": { + "after": { + "description": "Cursor for pagination. Use the endCursor from the previous page's PageInfo for GraphQL APIs.", + "type": "string" + }, + "direction": { + "description": "Order direction. If provided, the 'orderBy' also needs to be provided.", + "enum": [ + "ASC", + "DESC" + ], + "type": "string" + }, + "labels": { + "description": "Filter by labels", + "items": { + "type": "string" + }, + "type": "array" + }, + "orderBy": { + "description": "Order issues by field. If provided, the 'direction' also needs to be provided.", + "enum": [ + "CREATED_AT", + "UPDATED_AT", + "COMMENTS" + ], + "type": "string" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + }, + "since": { + "description": "Filter by date (ISO 8601 timestamp)", + "type": "string" + }, + "state": { + "description": "Filter by state, by default both open and closed issues are returned when not provided", + "enum": [ + "OPEN", + "CLOSED" + ], + "type": "string" + } + }, + "required": [ + "owner", + "repo" + ], + "type": "object" + }, + "name": "list_issues" + }, + { + "annotations": { + "read_only": true + }, + "description": "List pull requests in a GitHub repository. If the user specifies an author, then DO NOT use this tool and use the search_pull_requests tool instead.", + "input_schema": { + "properties": { + "base": { + "description": "Filter by base branch", + "type": "string" + }, + "direction": { + "description": "Sort direction", + "enum": [ + "asc", + "desc" + ], + "type": "string" + }, + "head": { + "description": "Filter by head user/org and branch", + "type": "string" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "page": { + "description": "Page number for pagination (min 1)", + "minimum": 1, + "type": "number" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + }, + "sort": { + "description": "Sort by", + "enum": [ + "created", + "updated", + "popularity", + "long-running" + ], + "type": "string" + }, + "state": { + "description": "Filter by state", + "enum": [ + "open", + "closed", + "all" + ], + "type": "string" + } + }, + "required": [ + "owner", + "repo" + ], + "type": "object" + }, + "name": "list_pull_requests" + }, + { + "annotations": { + "read_only": true + }, + "description": "List releases in a GitHub repository", + "input_schema": { + "properties": { + "owner": { + "description": "Repository owner", + "type": "string" + }, + "page": { + "description": "Page number for pagination (min 1)", + "minimum": 1, + "type": "number" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo" + ], + "type": "object" + }, + "name": "list_releases" + }, + { + "annotations": { + "read_only": true + }, + "description": "List git tags in a GitHub repository", + "input_schema": { + "properties": { + "owner": { + "description": "Repository owner", + "type": "string" + }, + "page": { + "description": "Page number for pagination (min 1)", + "minimum": 1, + "type": "number" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo" + ], + "type": "object" + }, + "name": "list_tags" + }, + { + "annotations": { + "read_only": false + }, + "description": "Merge a pull request in a GitHub repository.", + "input_schema": { + "properties": { + "commit_message": { + "description": "Extra detail for merge commit", + "type": "string" + }, + "commit_title": { + "description": "Title for merge commit", + "type": "string" + }, + "merge_method": { + "description": "Merge method", + "enum": [ + "merge", + "squash", + "rebase" + ], + "type": "string" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "pullNumber": { + "description": "Pull request number", + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "pullNumber" + ], + "type": "object" + }, + "name": "merge_pull_request" + }, + { + "annotations": { + "read_only": true + }, + "description": "Get information on a specific pull request in GitHub repository.", + "input_schema": { + "properties": { + "method": { + "description": "Action to specify what pull request data needs to be retrieved from GitHub. \nPossible options: \n 1. get - Get details of a specific pull request.\n 2. get_diff - Get the diff of a pull request.\n 3. get_status - Get status of a head commit in a pull request. This reflects status of builds and checks.\n 4. get_files - Get the list of files changed in a pull request. Use with pagination parameters to control the number of results returned.\n 5. get_review_comments - Get the review comments on a pull request. They are comments made on a portion of the unified diff during a pull request review. Use with pagination parameters to control the number of results returned.\n 6. get_reviews - Get the reviews on a pull request. When asked for review comments, use get_review_comments method.\n 7. get_comments - Get comments on a pull request. Use this if user doesn't specifically want review comments. Use with pagination parameters to control the number of results returned.\n", + "enum": [ + "get", + "get_diff", + "get_status", + "get_files", + "get_review_comments", + "get_reviews", + "get_comments" + ], + "type": "string" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "page": { + "description": "Page number for pagination (min 1)", + "minimum": 1, + "type": "number" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "pullNumber": { + "description": "Pull request number", + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "method", + "owner", + "repo", + "pullNumber" + ], + "type": "object" + }, + "name": "pull_request_read" + }, + { + "annotations": { + "read_only": false + }, + "description": "Create and/or submit, delete review of a pull request.\n\nAvailable methods:\n- create: Create a new review of a pull request. If \"event\" parameter is provided, the review is submitted. If \"event\" is omitted, a pending review is created.\n- submit_pending: Submit an existing pending review of a pull request. This requires that a pending review exists for the current user on the specified pull request. The \"body\" and \"event\" parameters are used when submitting the review.\n- delete_pending: Delete an existing pending review of a pull request. This requires that a pending review exists for the current user on the specified pull request.\n", + "input_schema": { + "properties": { + "body": { + "description": "Review comment text", + "type": "string" + }, + "commitID": { + "description": "SHA of commit to review", + "type": "string" + }, + "event": { + "description": "Review action to perform.", + "enum": [ + "APPROVE", + "REQUEST_CHANGES", + "COMMENT" + ], + "type": "string" + }, + "method": { + "description": "The write operation to perform on pull request review.", + "enum": [ + "create", + "submit_pending", + "delete_pending" + ], + "type": "string" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "pullNumber": { + "description": "Pull request number", + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "method", + "owner", + "repo", + "pullNumber" + ], + "type": "object" + }, + "name": "pull_request_review_write" + }, + { + "annotations": { + "read_only": false + }, + "description": "Push multiple files to a GitHub repository in a single commit", + "input_schema": { + "properties": { + "branch": { + "description": "Branch to push to", + "type": "string" + }, + "files": { + "description": "Array of file objects to push, each object with path (string) and content (string)", + "items": { + "additionalProperties": false, + "properties": { + "content": { + "description": "file content", + "type": "string" + }, + "path": { + "description": "path to the file", + "type": "string" + } + }, + "required": [ + "path", + "content" + ], + "type": "object" + }, + "type": "array" + }, + "message": { + "description": "Commit message", + "type": "string" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "branch", + "files", + "message" + ], + "type": "object" + }, + "name": "push_files" + }, + { + "annotations": { + "read_only": false + }, + "description": "Request a GitHub Copilot code review for a pull request. Use this for automated feedback on pull requests, usually before requesting a human reviewer.", + "input_schema": { + "properties": { + "owner": { + "description": "Repository owner", + "type": "string" + }, + "pullNumber": { + "description": "Pull request number", + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "pullNumber" + ], + "type": "object" + }, + "name": "request_copilot_review" + }, + { + "annotations": { + "read_only": true + }, + "description": "Fast and precise code search across ALL GitHub repositories using GitHub's native search engine. Best for finding exact symbols, functions, classes, or specific code patterns.", + "input_schema": { + "properties": { + "order": { + "description": "Sort order for results", + "enum": [ + "asc", + "desc" + ], + "type": "string" + }, + "page": { + "description": "Page number for pagination (min 1)", + "minimum": 1, + "type": "number" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "query": { + "description": "Search query using GitHub's powerful code search syntax. Examples: 'content:Skill language:Java org:github', 'NOT is:archived language:Python OR language:go', 'repo:github/github-mcp-server'. Supports exact matching, language filters, path filters, and more.", + "type": "string" + }, + "sort": { + "description": "Sort field ('indexed' only)", + "type": "string" + } + }, + "required": [ + "query" + ], + "type": "object" + }, + "name": "search_code" + }, + { + "annotations": { + "read_only": true + }, + "description": "Search for issues in GitHub repositories using issues search syntax already scoped to is:issue", + "input_schema": { + "properties": { + "order": { + "description": "Sort order", + "enum": [ + "asc", + "desc" + ], + "type": "string" + }, + "owner": { + "description": "Optional repository owner. If provided with repo, only issues for this repository are listed.", + "type": "string" + }, + "page": { + "description": "Page number for pagination (min 1)", + "minimum": 1, + "type": "number" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "query": { + "description": "Search query using GitHub issues search syntax", + "type": "string" + }, + "repo": { + "description": "Optional repository name. If provided with owner, only issues for this repository are listed.", + "type": "string" + }, + "sort": { + "description": "Sort field by number of matches of categories, defaults to best match", + "enum": [ + "comments", + "reactions", + "reactions-+1", + "reactions--1", + "reactions-smile", + "reactions-thinking_face", + "reactions-heart", + "reactions-tada", + "interactions", + "created", + "updated" + ], + "type": "string" + } + }, + "required": [ + "query" + ], + "type": "object" + }, + "name": "search_issues" + }, + { + "annotations": { + "read_only": true + }, + "description": "Search for pull requests in GitHub repositories using issues search syntax already scoped to is:pr", + "input_schema": { + "properties": { + "order": { + "description": "Sort order", + "enum": [ + "asc", + "desc" + ], + "type": "string" + }, + "owner": { + "description": "Optional repository owner. If provided with repo, only pull requests for this repository are listed.", + "type": "string" + }, + "page": { + "description": "Page number for pagination (min 1)", + "minimum": 1, + "type": "number" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "query": { + "description": "Search query using GitHub pull request search syntax", + "type": "string" + }, + "repo": { + "description": "Optional repository name. If provided with owner, only pull requests for this repository are listed.", + "type": "string" + }, + "sort": { + "description": "Sort field by number of matches of categories, defaults to best match", + "enum": [ + "comments", + "reactions", + "reactions-+1", + "reactions--1", + "reactions-smile", + "reactions-thinking_face", + "reactions-heart", + "reactions-tada", + "interactions", + "created", + "updated" + ], + "type": "string" + } + }, + "required": [ + "query" + ], + "type": "object" + }, + "name": "search_pull_requests" + }, + { + "annotations": { + "read_only": true + }, + "description": "Find GitHub repositories by name, description, readme, topics, or other metadata. Perfect for discovering projects, finding examples, or locating specific repositories across GitHub.", + "input_schema": { + "properties": { + "minimal_output": { + "default": true, + "description": "Return minimal repository information (default: true). When false, returns full GitHub API repository objects.", + "type": "boolean" + }, + "order": { + "description": "Sort order", + "enum": [ + "asc", + "desc" + ], + "type": "string" + }, + "page": { + "description": "Page number for pagination (min 1)", + "minimum": 1, + "type": "number" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "query": { + "description": "Repository search query. Examples: 'machine learning in:name stars:>1000 language:python', 'topic:react', 'user:facebook'. Supports advanced search syntax for precise filtering.", + "type": "string" + }, + "sort": { + "description": "Sort repositories by field, defaults to best match", + "enum": [ + "stars", + "forks", + "help-wanted-issues", + "updated" + ], + "type": "string" + } + }, + "required": [ + "query" + ], + "type": "object" + }, + "name": "search_repositories" + }, + { + "annotations": { + "read_only": true + }, + "description": "Find GitHub users by username, real name, or other profile information. Useful for locating developers, contributors, or team members.", + "input_schema": { + "properties": { + "order": { + "description": "Sort order", + "enum": [ + "asc", + "desc" + ], + "type": "string" + }, + "page": { + "description": "Page number for pagination (min 1)", + "minimum": 1, + "type": "number" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "query": { + "description": "User search query. Examples: 'john smith', 'location:seattle', 'followers:>100'. Search is automatically scoped to type:user.", + "type": "string" + }, + "sort": { + "description": "Sort users by number of followers or repositories, or when the person joined GitHub.", + "enum": [ + "followers", + "repositories", + "joined" + ], + "type": "string" + } + }, + "required": [ + "query" + ], + "type": "object" + }, + "name": "search_users" + }, + { + "annotations": { + "read_only": false + }, + "description": "Add a sub-issue to a parent issue in a GitHub repository.", + "input_schema": { + "properties": { + "after_id": { + "description": "The ID of the sub-issue to be prioritized after (either after_id OR before_id should be specified)", + "type": "number" + }, + "before_id": { + "description": "The ID of the sub-issue to be prioritized before (either after_id OR before_id should be specified)", + "type": "number" + }, + "issue_number": { + "description": "The number of the parent issue", + "type": "number" + }, + "method": { + "description": "The action to perform on a single sub-issue\nOptions are:\n- 'add' - add a sub-issue to a parent issue in a GitHub repository.\n- 'remove' - remove a sub-issue from a parent issue in a GitHub repository.\n- 'reprioritize' - change the order of sub-issues within a parent issue in a GitHub repository. Use either 'after_id' or 'before_id' to specify the new position.\n\t\t\t\t", + "type": "string" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "replace_parent": { + "description": "When true, replaces the sub-issue's current parent issue. Use with 'add' method only.", + "type": "boolean" + }, + "repo": { + "description": "Repository name", + "type": "string" + }, + "sub_issue_id": { + "description": "The ID of the sub-issue to add. ID is not the same as issue number", + "type": "number" + } + }, + "required": [ + "method", + "owner", + "repo", + "issue_number", + "sub_issue_id" + ], + "type": "object" + }, + "name": "sub_issue_write" + }, + { + "annotations": { + "read_only": false + }, + "description": "Update an existing pull request in a GitHub repository.", + "input_schema": { + "properties": { + "base": { + "description": "New base branch name", + "type": "string" + }, + "body": { + "description": "New description", + "type": "string" + }, + "draft": { + "description": "Mark pull request as draft (true) or ready for review (false)", + "type": "boolean" + }, + "maintainer_can_modify": { + "description": "Allow maintainer edits", + "type": "boolean" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "pullNumber": { + "description": "Pull request number to update", + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + }, + "reviewers": { + "description": "GitHub usernames to request reviews from", + "items": { + "type": "string" + }, + "type": "array" + }, + "state": { + "description": "New state", + "enum": [ + "open", + "closed" + ], + "type": "string" + }, + "title": { + "description": "New title", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "pullNumber" + ], + "type": "object" + }, + "name": "update_pull_request" + }, + { + "annotations": { + "read_only": false + }, + "description": "Update the branch of a pull request with the latest changes from the base branch.", + "input_schema": { + "properties": { + "expectedHeadSha": { + "description": "The expected SHA of the pull request's HEAD ref", + "type": "string" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "pullNumber": { + "description": "Pull request number", + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "pullNumber" + ], + "type": "object" + }, + "name": "update_pull_request_branch" + } + ] + }, + { + "id": "msg_05c47f40bf062be000691e3432aad0819b95d41198acf32dfd", + "type": "message", + "status": "completed", + "content": [ + { + "type": "output_text", + "annotations": [], + "logprobs": [], + "text": "I can help with a variety of tasks, such as:\n\n1. **Answering Questions**: Providing information on a wide range of topics.\n2. **Text Analysis**: Summarizing, translating, or rephrasing text.\n3. **Technical Support**: Offering programming help, code review, and technical explanations.\n4. **GitHub Management**: Assisting with issues, pull requests, and repository management directly on GitHub.\n5. **Image Analysis**: Describing and interpreting images that you provide.\n\nFeel free to ask about anything specific you need!" + } + ], + "role": "assistant" + } + ], + "parallel_tool_calls": true, + "previous_response_id": null, + "prompt_cache_key": null, + "prompt_cache_retention": null, + "reasoning": { + "effort": null, + "summary": null + }, + "safety_identifier": null, + "service_tier": "default", + "store": true, + "temperature": 1.0, + "text": { + "format": { + "type": "text" + }, + "verbosity": "medium" + }, + "tool_choice": "auto", + "tools": [ + { + "type": "mcp", + "allowed_tools": null, + "authorization": "", + "headers": null, + "require_approval": "always", + "server_description": null, + "server_label": "github", + "server_url": "https://api.githubcopilot.com/mcp/" + } + ], + "top_logprobs": 0, + "top_p": 1.0, + "truncation": "disabled", + "usage": { + "input_tokens": 5237, + "input_tokens_details": { + "cached_tokens": 0 + }, + "output_tokens": 118, + "output_tokens_details": { + "reasoning_tokens": 0 + }, + "total_tokens": 5355 + }, + "user": null, + "metadata": {} + } + recorded_at: Wed, 19 Nov 2025 21:18:45 GMT +recorded_with: VCR 6.3.1 diff --git a/test/fixtures/vcr_cassettes/docs/actions/mcps/openai_custom_servers.yml b/test/fixtures/vcr_cassettes/docs/actions/mcps/openai_custom_servers.yml new file mode 100644 index 00000000..fbd78f87 --- /dev/null +++ b/test/fixtures/vcr_cassettes/docs/actions/mcps/openai_custom_servers.yml @@ -0,0 +1,1906 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.openai.com/v1/responses + body: + encoding: UTF-8 + string: '{"model":"gpt-4o","input":"Use custom tools","tools":[{"type":"mcp","server_label":"github_copilot","server_url":"https://api.githubcopilot.com/mcp/","authorization":"GITHUB_MCP_TOKEN"}]}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json + User-Agent: + - OpenAI::Client/Ruby 0.35.1 + Host: + - api.openai.com + X-Stainless-Arch: + - arm64 + X-Stainless-Lang: + - ruby + X-Stainless-Os: + - MacOS + X-Stainless-Package-Version: + - 0.35.1 + X-Stainless-Runtime: + - ruby + X-Stainless-Runtime-Version: + - 3.4.7 + Content-Type: + - application/json + Openai-Organization: + - ORGANIZATION_ID + Openai-Project: + - PROJECT_ID + Authorization: + - Bearer ACCESS_TOKEN + X-Stainless-Retry-Count: + - '0' + X-Stainless-Timeout: + - '600.0' + Content-Length: + - '264' + response: + status: + code: 200 + message: OK + headers: + Date: + - Wed, 19 Nov 2025 21:17:46 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + X-Ratelimit-Limit-Requests: + - '10000' + X-Ratelimit-Limit-Tokens: + - '30000000' + X-Ratelimit-Remaining-Requests: + - '9999' + X-Ratelimit-Remaining-Tokens: + - '29994737' + X-Ratelimit-Reset-Requests: + - 6ms + X-Ratelimit-Reset-Tokens: + - 10ms + Openai-Version: + - '2020-10-01' + Openai-Organization: + - ORGANIZATION_ID + Openai-Project: + - PROJECT_ID + X-Request-Id: + - req_39630111118f41dfa03a34e3067af926 + Openai-Processing-Ms: + - '3750' + X-Envoy-Upstream-Service-Time: + - '3755' + Cf-Cache-Status: + - DYNAMIC + Set-Cookie: + - __cf_bm=i5A0axULRCd4mwO13MroyHyKJMLB_m9yEujHaNOqjas-1763587066-1.0.1.1-QkxtATDzVMy9l.m1N0mvECilaMuCkUoYNn3TSB9tbMdjfpGUco103.RwSpq3TVLql50YF2GOBM.FFYEFzSwM4g8NcoYdx6DdsMd3IlYuMNc; + path=/; expires=Wed, 19-Nov-25 21:47:46 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=aJjPuz.w9X21XPKVf1c3EYfGWQlmaxNlzZ_HhafeMjM-1763587066219-0.0.1.1-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + X-Content-Type-Options: + - nosniff + Server: + - cloudflare + Cf-Ray: + - 9a12bc640dd1c487-SJC + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: |- + { + "id": "resp_0fcc7d91cd79b3ca00691e33f675148197b0046f47eabad5a6", + "object": "response", + "created_at": 1763587062, + "status": "completed", + "background": false, + "billing": { + "payer": "developer" + }, + "error": null, + "incomplete_details": null, + "instructions": null, + "max_output_tokens": null, + "max_tool_calls": null, + "model": "gpt-4o-2024-08-06", + "output": [ + { + "id": "mcpl_0fcc7d91cd79b3ca00691e33f6c8788197b26a74c889b5d877", + "type": "mcp_list_tools", + "server_label": "github_copilot", + "tools": [ + { + "annotations": { + "read_only": false + }, + "description": "Add review comment to the requester's latest pending pull request review. A pending review needs to already exist to call this (check with the user if not sure).", + "input_schema": { + "properties": { + "body": { + "description": "The text of the review comment", + "type": "string" + }, + "line": { + "description": "The line of the blob in the pull request diff that the comment applies to. For multi-line comments, the last line of the range", + "type": "number" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "path": { + "description": "The relative path to the file that necessitates a comment", + "type": "string" + }, + "pullNumber": { + "description": "Pull request number", + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + }, + "side": { + "description": "The side of the diff to comment on. LEFT indicates the previous state, RIGHT indicates the new state", + "enum": [ + "LEFT", + "RIGHT" + ], + "type": "string" + }, + "startLine": { + "description": "For multi-line comments, the first line of the range that the comment applies to", + "type": "number" + }, + "startSide": { + "description": "For multi-line comments, the starting side of the diff that the comment applies to. LEFT indicates the previous state, RIGHT indicates the new state", + "enum": [ + "LEFT", + "RIGHT" + ], + "type": "string" + }, + "subjectType": { + "description": "The level at which the comment is targeted", + "enum": [ + "FILE", + "LINE" + ], + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "pullNumber", + "path", + "body", + "subjectType" + ], + "type": "object" + }, + "name": "add_comment_to_pending_review" + }, + { + "annotations": { + "read_only": false + }, + "description": "Add a comment to a specific issue in a GitHub repository. Use this tool to add comments to pull requests as well (in this case pass pull request number as issue_number), but only if user is not asking specifically to add review comments.", + "input_schema": { + "properties": { + "body": { + "description": "Comment content", + "type": "string" + }, + "issue_number": { + "description": "Issue number to comment on", + "type": "number" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "issue_number", + "body" + ], + "type": "object" + }, + "name": "add_issue_comment" + }, + { + "annotations": { + "read_only": false + }, + "description": "Assign Copilot to a specific issue in a GitHub repository.\n\nThis tool can help with the following outcomes:\n- a Pull Request created with source code changes to resolve the issue\n\n\nMore information can be found at:\n- https://docs.github.com/en/copilot/using-github-copilot/using-copilot-coding-agent-to-work-on-tasks/about-assigning-tasks-to-copilot\n", + "input_schema": { + "properties": { + "issueNumber": { + "description": "Issue number", + "type": "number" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "issueNumber" + ], + "type": "object" + }, + "name": "assign_copilot_to_issue" + }, + { + "annotations": { + "read_only": false + }, + "description": "Create a new branch in a GitHub repository", + "input_schema": { + "properties": { + "branch": { + "description": "Name for new branch", + "type": "string" + }, + "from_branch": { + "description": "Source branch (defaults to repo default)", + "type": "string" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "branch" + ], + "type": "object" + }, + "name": "create_branch" + }, + { + "annotations": { + "read_only": false + }, + "description": "Create or update a single file in a GitHub repository. If updating, you must provide the SHA of the file you want to update. Use this tool to create or update a file in a GitHub repository remotely; do not use it for local file operations.", + "input_schema": { + "properties": { + "branch": { + "description": "Branch to create/update the file in", + "type": "string" + }, + "content": { + "description": "Content of the file", + "type": "string" + }, + "message": { + "description": "Commit message", + "type": "string" + }, + "owner": { + "description": "Repository owner (username or organization)", + "type": "string" + }, + "path": { + "description": "Path where to create/update the file", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + }, + "sha": { + "description": "Required if updating an existing file. The blob SHA of the file being replaced.", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "path", + "content", + "message", + "branch" + ], + "type": "object" + }, + "name": "create_or_update_file" + }, + { + "annotations": { + "read_only": false + }, + "description": "Create a new pull request in a GitHub repository.", + "input_schema": { + "properties": { + "base": { + "description": "Branch to merge into", + "type": "string" + }, + "body": { + "description": "PR description", + "type": "string" + }, + "draft": { + "description": "Create as draft PR", + "type": "boolean" + }, + "head": { + "description": "Branch containing changes", + "type": "string" + }, + "maintainer_can_modify": { + "description": "Allow maintainer edits", + "type": "boolean" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + }, + "title": { + "description": "PR title", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "title", + "head", + "base" + ], + "type": "object" + }, + "name": "create_pull_request" + }, + { + "annotations": { + "read_only": false + }, + "description": "Create a new GitHub repository in your account or specified organization", + "input_schema": { + "properties": { + "autoInit": { + "description": "Initialize with README", + "type": "boolean" + }, + "description": { + "description": "Repository description", + "type": "string" + }, + "name": { + "description": "Repository name", + "type": "string" + }, + "organization": { + "description": "Organization to create the repository in (omit to create in your personal account)", + "type": "string" + }, + "private": { + "description": "Whether repo should be private", + "type": "boolean" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "name": "create_repository" + }, + { + "annotations": { + "read_only": false + }, + "description": "Delete a file from a GitHub repository", + "input_schema": { + "properties": { + "branch": { + "description": "Branch to delete the file from", + "type": "string" + }, + "message": { + "description": "Commit message", + "type": "string" + }, + "owner": { + "description": "Repository owner (username or organization)", + "type": "string" + }, + "path": { + "description": "Path to the file to delete", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "path", + "message", + "branch" + ], + "type": "object" + }, + "name": "delete_file" + }, + { + "annotations": { + "read_only": false + }, + "description": "Fork a GitHub repository to your account or specified organization", + "input_schema": { + "properties": { + "organization": { + "description": "Organization to fork to", + "type": "string" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo" + ], + "type": "object" + }, + "name": "fork_repository" + }, + { + "annotations": { + "read_only": true + }, + "description": "Get details for a commit from a GitHub repository", + "input_schema": { + "properties": { + "include_diff": { + "default": true, + "description": "Whether to include file diffs and stats in the response. Default is true.", + "type": "boolean" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "page": { + "description": "Page number for pagination (min 1)", + "minimum": 1, + "type": "number" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + }, + "sha": { + "description": "Commit SHA, branch name, or tag name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "sha" + ], + "type": "object" + }, + "name": "get_commit" + }, + { + "annotations": { + "read_only": true + }, + "description": "Get the contents of a file or directory from a GitHub repository", + "input_schema": { + "properties": { + "owner": { + "description": "Repository owner (username or organization)", + "type": "string" + }, + "path": { + "default": "/", + "description": "Path to file/directory (directories must end with a slash '/')", + "type": "string" + }, + "ref": { + "description": "Accepts optional git refs such as `refs/tags/{tag}`, `refs/heads/{branch}` or `refs/pull/{pr_number}/head`", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + }, + "sha": { + "description": "Accepts optional commit SHA. If specified, it will be used instead of ref", + "type": "string" + } + }, + "required": [ + "owner", + "repo" + ], + "type": "object" + }, + "name": "get_file_contents" + }, + { + "annotations": { + "read_only": true + }, + "description": "Get a specific label from a repository.", + "input_schema": { + "properties": { + "name": { + "description": "Label name.", + "type": "string" + }, + "owner": { + "description": "Repository owner (username or organization name)", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "name" + ], + "type": "object" + }, + "name": "get_label" + }, + { + "annotations": { + "read_only": true + }, + "description": "Get the latest release in a GitHub repository", + "input_schema": { + "properties": { + "owner": { + "description": "Repository owner", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo" + ], + "type": "object" + }, + "name": "get_latest_release" + }, + { + "annotations": { + "read_only": true + }, + "description": "Get details of the authenticated GitHub user. Use this when a request is about the user's own profile for GitHub. Or when information is missing to build other tool calls.", + "input_schema": { + "properties": {}, + "type": "object" + }, + "name": "get_me" + }, + { + "annotations": { + "read_only": true + }, + "description": "Get a specific release by its tag name in a GitHub repository", + "input_schema": { + "properties": { + "owner": { + "description": "Repository owner", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + }, + "tag": { + "description": "Tag name (e.g., 'v1.0.0')", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "tag" + ], + "type": "object" + }, + "name": "get_release_by_tag" + }, + { + "annotations": { + "read_only": true + }, + "description": "Get details about a specific git tag in a GitHub repository", + "input_schema": { + "properties": { + "owner": { + "description": "Repository owner", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + }, + "tag": { + "description": "Tag name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "tag" + ], + "type": "object" + }, + "name": "get_tag" + }, + { + "annotations": { + "read_only": true + }, + "description": "Get member usernames of a specific team in an organization. Limited to organizations accessible with current credentials", + "input_schema": { + "properties": { + "org": { + "description": "Organization login (owner) that contains the team.", + "type": "string" + }, + "team_slug": { + "description": "Team slug", + "type": "string" + } + }, + "required": [ + "org", + "team_slug" + ], + "type": "object" + }, + "name": "get_team_members" + }, + { + "annotations": { + "read_only": true + }, + "description": "Get details of the teams the user is a member of. Limited to organizations accessible with current credentials", + "input_schema": { + "properties": { + "user": { + "description": "Username to get teams for. If not provided, uses the authenticated user.", + "type": "string" + } + }, + "type": "object" + }, + "name": "get_teams" + }, + { + "annotations": { + "read_only": true + }, + "description": "Get information about a specific issue in a GitHub repository.", + "input_schema": { + "properties": { + "issue_number": { + "description": "The number of the issue", + "type": "number" + }, + "method": { + "description": "The read operation to perform on a single issue. \nOptions are: \n1. get - Get details of a specific issue.\n2. get_comments - Get issue comments.\n3. get_sub_issues - Get sub-issues of the issue.\n4. get_labels - Get labels assigned to the issue.\n", + "enum": [ + "get", + "get_comments", + "get_sub_issues", + "get_labels" + ], + "type": "string" + }, + "owner": { + "description": "The owner of the repository", + "type": "string" + }, + "page": { + "description": "Page number for pagination (min 1)", + "minimum": 1, + "type": "number" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "repo": { + "description": "The name of the repository", + "type": "string" + } + }, + "required": [ + "method", + "owner", + "repo", + "issue_number" + ], + "type": "object" + }, + "name": "issue_read" + }, + { + "annotations": { + "read_only": false + }, + "description": "Create a new or update an existing issue in a GitHub repository.", + "input_schema": { + "properties": { + "assignees": { + "description": "Usernames to assign to this issue", + "items": { + "type": "string" + }, + "type": "array" + }, + "body": { + "description": "Issue body content", + "type": "string" + }, + "duplicate_of": { + "description": "Issue number that this issue is a duplicate of. Only used when state_reason is 'duplicate'.", + "type": "number" + }, + "issue_number": { + "description": "Issue number to update", + "type": "number" + }, + "labels": { + "description": "Labels to apply to this issue", + "items": { + "type": "string" + }, + "type": "array" + }, + "method": { + "description": "Write operation to perform on a single issue.\nOptions are: \n- 'create' - creates a new issue. \n- 'update' - updates an existing issue.\n", + "enum": [ + "create", + "update" + ], + "type": "string" + }, + "milestone": { + "description": "Milestone number", + "type": "number" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + }, + "state": { + "description": "New state", + "enum": [ + "open", + "closed" + ], + "type": "string" + }, + "state_reason": { + "description": "Reason for the state change. Ignored unless state is changed.", + "enum": [ + "completed", + "not_planned", + "duplicate" + ], + "type": "string" + }, + "title": { + "description": "Issue title", + "type": "string" + }, + "type": { + "description": "Type of this issue. Only use if the repository has issue types configured. Use list_issue_types tool to get valid type values for the organization. If the repository doesn't support issue types, omit this parameter.", + "type": "string" + } + }, + "required": [ + "method", + "owner", + "repo" + ], + "type": "object" + }, + "name": "issue_write" + }, + { + "annotations": { + "read_only": true + }, + "description": "List branches in a GitHub repository", + "input_schema": { + "properties": { + "owner": { + "description": "Repository owner", + "type": "string" + }, + "page": { + "description": "Page number for pagination (min 1)", + "minimum": 1, + "type": "number" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo" + ], + "type": "object" + }, + "name": "list_branches" + }, + { + "annotations": { + "read_only": true + }, + "description": "Get list of commits of a branch in a GitHub repository. Returns at least 30 results per page by default, but can return more if specified using the perPage parameter (up to 100).", + "input_schema": { + "properties": { + "author": { + "description": "Author username or email address to filter commits by", + "type": "string" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "page": { + "description": "Page number for pagination (min 1)", + "minimum": 1, + "type": "number" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + }, + "sha": { + "description": "Commit SHA, branch or tag name to list commits of. If not provided, uses the default branch of the repository. If a commit SHA is provided, will list commits up to that SHA.", + "type": "string" + } + }, + "required": [ + "owner", + "repo" + ], + "type": "object" + }, + "name": "list_commits" + }, + { + "annotations": { + "read_only": true + }, + "description": "List supported issue types for repository owner (organization).", + "input_schema": { + "properties": { + "owner": { + "description": "The organization owner of the repository", + "type": "string" + } + }, + "required": [ + "owner" + ], + "type": "object" + }, + "name": "list_issue_types" + }, + { + "annotations": { + "read_only": true + }, + "description": "List issues in a GitHub repository. For pagination, use the 'endCursor' from the previous response's 'pageInfo' in the 'after' parameter.", + "input_schema": { + "properties": { + "after": { + "description": "Cursor for pagination. Use the endCursor from the previous page's PageInfo for GraphQL APIs.", + "type": "string" + }, + "direction": { + "description": "Order direction. If provided, the 'orderBy' also needs to be provided.", + "enum": [ + "ASC", + "DESC" + ], + "type": "string" + }, + "labels": { + "description": "Filter by labels", + "items": { + "type": "string" + }, + "type": "array" + }, + "orderBy": { + "description": "Order issues by field. If provided, the 'direction' also needs to be provided.", + "enum": [ + "CREATED_AT", + "UPDATED_AT", + "COMMENTS" + ], + "type": "string" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + }, + "since": { + "description": "Filter by date (ISO 8601 timestamp)", + "type": "string" + }, + "state": { + "description": "Filter by state, by default both open and closed issues are returned when not provided", + "enum": [ + "OPEN", + "CLOSED" + ], + "type": "string" + } + }, + "required": [ + "owner", + "repo" + ], + "type": "object" + }, + "name": "list_issues" + }, + { + "annotations": { + "read_only": true + }, + "description": "List pull requests in a GitHub repository. If the user specifies an author, then DO NOT use this tool and use the search_pull_requests tool instead.", + "input_schema": { + "properties": { + "base": { + "description": "Filter by base branch", + "type": "string" + }, + "direction": { + "description": "Sort direction", + "enum": [ + "asc", + "desc" + ], + "type": "string" + }, + "head": { + "description": "Filter by head user/org and branch", + "type": "string" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "page": { + "description": "Page number for pagination (min 1)", + "minimum": 1, + "type": "number" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + }, + "sort": { + "description": "Sort by", + "enum": [ + "created", + "updated", + "popularity", + "long-running" + ], + "type": "string" + }, + "state": { + "description": "Filter by state", + "enum": [ + "open", + "closed", + "all" + ], + "type": "string" + } + }, + "required": [ + "owner", + "repo" + ], + "type": "object" + }, + "name": "list_pull_requests" + }, + { + "annotations": { + "read_only": true + }, + "description": "List releases in a GitHub repository", + "input_schema": { + "properties": { + "owner": { + "description": "Repository owner", + "type": "string" + }, + "page": { + "description": "Page number for pagination (min 1)", + "minimum": 1, + "type": "number" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo" + ], + "type": "object" + }, + "name": "list_releases" + }, + { + "annotations": { + "read_only": true + }, + "description": "List git tags in a GitHub repository", + "input_schema": { + "properties": { + "owner": { + "description": "Repository owner", + "type": "string" + }, + "page": { + "description": "Page number for pagination (min 1)", + "minimum": 1, + "type": "number" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo" + ], + "type": "object" + }, + "name": "list_tags" + }, + { + "annotations": { + "read_only": false + }, + "description": "Merge a pull request in a GitHub repository.", + "input_schema": { + "properties": { + "commit_message": { + "description": "Extra detail for merge commit", + "type": "string" + }, + "commit_title": { + "description": "Title for merge commit", + "type": "string" + }, + "merge_method": { + "description": "Merge method", + "enum": [ + "merge", + "squash", + "rebase" + ], + "type": "string" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "pullNumber": { + "description": "Pull request number", + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "pullNumber" + ], + "type": "object" + }, + "name": "merge_pull_request" + }, + { + "annotations": { + "read_only": true + }, + "description": "Get information on a specific pull request in GitHub repository.", + "input_schema": { + "properties": { + "method": { + "description": "Action to specify what pull request data needs to be retrieved from GitHub. \nPossible options: \n 1. get - Get details of a specific pull request.\n 2. get_diff - Get the diff of a pull request.\n 3. get_status - Get status of a head commit in a pull request. This reflects status of builds and checks.\n 4. get_files - Get the list of files changed in a pull request. Use with pagination parameters to control the number of results returned.\n 5. get_review_comments - Get the review comments on a pull request. They are comments made on a portion of the unified diff during a pull request review. Use with pagination parameters to control the number of results returned.\n 6. get_reviews - Get the reviews on a pull request. When asked for review comments, use get_review_comments method.\n 7. get_comments - Get comments on a pull request. Use this if user doesn't specifically want review comments. Use with pagination parameters to control the number of results returned.\n", + "enum": [ + "get", + "get_diff", + "get_status", + "get_files", + "get_review_comments", + "get_reviews", + "get_comments" + ], + "type": "string" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "page": { + "description": "Page number for pagination (min 1)", + "minimum": 1, + "type": "number" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "pullNumber": { + "description": "Pull request number", + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "method", + "owner", + "repo", + "pullNumber" + ], + "type": "object" + }, + "name": "pull_request_read" + }, + { + "annotations": { + "read_only": false + }, + "description": "Create and/or submit, delete review of a pull request.\n\nAvailable methods:\n- create: Create a new review of a pull request. If \"event\" parameter is provided, the review is submitted. If \"event\" is omitted, a pending review is created.\n- submit_pending: Submit an existing pending review of a pull request. This requires that a pending review exists for the current user on the specified pull request. The \"body\" and \"event\" parameters are used when submitting the review.\n- delete_pending: Delete an existing pending review of a pull request. This requires that a pending review exists for the current user on the specified pull request.\n", + "input_schema": { + "properties": { + "body": { + "description": "Review comment text", + "type": "string" + }, + "commitID": { + "description": "SHA of commit to review", + "type": "string" + }, + "event": { + "description": "Review action to perform.", + "enum": [ + "APPROVE", + "REQUEST_CHANGES", + "COMMENT" + ], + "type": "string" + }, + "method": { + "description": "The write operation to perform on pull request review.", + "enum": [ + "create", + "submit_pending", + "delete_pending" + ], + "type": "string" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "pullNumber": { + "description": "Pull request number", + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "method", + "owner", + "repo", + "pullNumber" + ], + "type": "object" + }, + "name": "pull_request_review_write" + }, + { + "annotations": { + "read_only": false + }, + "description": "Push multiple files to a GitHub repository in a single commit", + "input_schema": { + "properties": { + "branch": { + "description": "Branch to push to", + "type": "string" + }, + "files": { + "description": "Array of file objects to push, each object with path (string) and content (string)", + "items": { + "additionalProperties": false, + "properties": { + "content": { + "description": "file content", + "type": "string" + }, + "path": { + "description": "path to the file", + "type": "string" + } + }, + "required": [ + "path", + "content" + ], + "type": "object" + }, + "type": "array" + }, + "message": { + "description": "Commit message", + "type": "string" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "branch", + "files", + "message" + ], + "type": "object" + }, + "name": "push_files" + }, + { + "annotations": { + "read_only": false + }, + "description": "Request a GitHub Copilot code review for a pull request. Use this for automated feedback on pull requests, usually before requesting a human reviewer.", + "input_schema": { + "properties": { + "owner": { + "description": "Repository owner", + "type": "string" + }, + "pullNumber": { + "description": "Pull request number", + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "pullNumber" + ], + "type": "object" + }, + "name": "request_copilot_review" + }, + { + "annotations": { + "read_only": true + }, + "description": "Fast and precise code search across ALL GitHub repositories using GitHub's native search engine. Best for finding exact symbols, functions, classes, or specific code patterns.", + "input_schema": { + "properties": { + "order": { + "description": "Sort order for results", + "enum": [ + "asc", + "desc" + ], + "type": "string" + }, + "page": { + "description": "Page number for pagination (min 1)", + "minimum": 1, + "type": "number" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "query": { + "description": "Search query using GitHub's powerful code search syntax. Examples: 'content:Skill language:Java org:github', 'NOT is:archived language:Python OR language:go', 'repo:github/github-mcp-server'. Supports exact matching, language filters, path filters, and more.", + "type": "string" + }, + "sort": { + "description": "Sort field ('indexed' only)", + "type": "string" + } + }, + "required": [ + "query" + ], + "type": "object" + }, + "name": "search_code" + }, + { + "annotations": { + "read_only": true + }, + "description": "Search for issues in GitHub repositories using issues search syntax already scoped to is:issue", + "input_schema": { + "properties": { + "order": { + "description": "Sort order", + "enum": [ + "asc", + "desc" + ], + "type": "string" + }, + "owner": { + "description": "Optional repository owner. If provided with repo, only issues for this repository are listed.", + "type": "string" + }, + "page": { + "description": "Page number for pagination (min 1)", + "minimum": 1, + "type": "number" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "query": { + "description": "Search query using GitHub issues search syntax", + "type": "string" + }, + "repo": { + "description": "Optional repository name. If provided with owner, only issues for this repository are listed.", + "type": "string" + }, + "sort": { + "description": "Sort field by number of matches of categories, defaults to best match", + "enum": [ + "comments", + "reactions", + "reactions-+1", + "reactions--1", + "reactions-smile", + "reactions-thinking_face", + "reactions-heart", + "reactions-tada", + "interactions", + "created", + "updated" + ], + "type": "string" + } + }, + "required": [ + "query" + ], + "type": "object" + }, + "name": "search_issues" + }, + { + "annotations": { + "read_only": true + }, + "description": "Search for pull requests in GitHub repositories using issues search syntax already scoped to is:pr", + "input_schema": { + "properties": { + "order": { + "description": "Sort order", + "enum": [ + "asc", + "desc" + ], + "type": "string" + }, + "owner": { + "description": "Optional repository owner. If provided with repo, only pull requests for this repository are listed.", + "type": "string" + }, + "page": { + "description": "Page number for pagination (min 1)", + "minimum": 1, + "type": "number" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "query": { + "description": "Search query using GitHub pull request search syntax", + "type": "string" + }, + "repo": { + "description": "Optional repository name. If provided with owner, only pull requests for this repository are listed.", + "type": "string" + }, + "sort": { + "description": "Sort field by number of matches of categories, defaults to best match", + "enum": [ + "comments", + "reactions", + "reactions-+1", + "reactions--1", + "reactions-smile", + "reactions-thinking_face", + "reactions-heart", + "reactions-tada", + "interactions", + "created", + "updated" + ], + "type": "string" + } + }, + "required": [ + "query" + ], + "type": "object" + }, + "name": "search_pull_requests" + }, + { + "annotations": { + "read_only": true + }, + "description": "Find GitHub repositories by name, description, readme, topics, or other metadata. Perfect for discovering projects, finding examples, or locating specific repositories across GitHub.", + "input_schema": { + "properties": { + "minimal_output": { + "default": true, + "description": "Return minimal repository information (default: true). When false, returns full GitHub API repository objects.", + "type": "boolean" + }, + "order": { + "description": "Sort order", + "enum": [ + "asc", + "desc" + ], + "type": "string" + }, + "page": { + "description": "Page number for pagination (min 1)", + "minimum": 1, + "type": "number" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "query": { + "description": "Repository search query. Examples: 'machine learning in:name stars:>1000 language:python', 'topic:react', 'user:facebook'. Supports advanced search syntax for precise filtering.", + "type": "string" + }, + "sort": { + "description": "Sort repositories by field, defaults to best match", + "enum": [ + "stars", + "forks", + "help-wanted-issues", + "updated" + ], + "type": "string" + } + }, + "required": [ + "query" + ], + "type": "object" + }, + "name": "search_repositories" + }, + { + "annotations": { + "read_only": true + }, + "description": "Find GitHub users by username, real name, or other profile information. Useful for locating developers, contributors, or team members.", + "input_schema": { + "properties": { + "order": { + "description": "Sort order", + "enum": [ + "asc", + "desc" + ], + "type": "string" + }, + "page": { + "description": "Page number for pagination (min 1)", + "minimum": 1, + "type": "number" + }, + "perPage": { + "description": "Results per page for pagination (min 1, max 100)", + "maximum": 100, + "minimum": 1, + "type": "number" + }, + "query": { + "description": "User search query. Examples: 'john smith', 'location:seattle', 'followers:>100'. Search is automatically scoped to type:user.", + "type": "string" + }, + "sort": { + "description": "Sort users by number of followers or repositories, or when the person joined GitHub.", + "enum": [ + "followers", + "repositories", + "joined" + ], + "type": "string" + } + }, + "required": [ + "query" + ], + "type": "object" + }, + "name": "search_users" + }, + { + "annotations": { + "read_only": false + }, + "description": "Add a sub-issue to a parent issue in a GitHub repository.", + "input_schema": { + "properties": { + "after_id": { + "description": "The ID of the sub-issue to be prioritized after (either after_id OR before_id should be specified)", + "type": "number" + }, + "before_id": { + "description": "The ID of the sub-issue to be prioritized before (either after_id OR before_id should be specified)", + "type": "number" + }, + "issue_number": { + "description": "The number of the parent issue", + "type": "number" + }, + "method": { + "description": "The action to perform on a single sub-issue\nOptions are:\n- 'add' - add a sub-issue to a parent issue in a GitHub repository.\n- 'remove' - remove a sub-issue from a parent issue in a GitHub repository.\n- 'reprioritize' - change the order of sub-issues within a parent issue in a GitHub repository. Use either 'after_id' or 'before_id' to specify the new position.\n\t\t\t\t", + "type": "string" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "replace_parent": { + "description": "When true, replaces the sub-issue's current parent issue. Use with 'add' method only.", + "type": "boolean" + }, + "repo": { + "description": "Repository name", + "type": "string" + }, + "sub_issue_id": { + "description": "The ID of the sub-issue to add. ID is not the same as issue number", + "type": "number" + } + }, + "required": [ + "method", + "owner", + "repo", + "issue_number", + "sub_issue_id" + ], + "type": "object" + }, + "name": "sub_issue_write" + }, + { + "annotations": { + "read_only": false + }, + "description": "Update an existing pull request in a GitHub repository.", + "input_schema": { + "properties": { + "base": { + "description": "New base branch name", + "type": "string" + }, + "body": { + "description": "New description", + "type": "string" + }, + "draft": { + "description": "Mark pull request as draft (true) or ready for review (false)", + "type": "boolean" + }, + "maintainer_can_modify": { + "description": "Allow maintainer edits", + "type": "boolean" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "pullNumber": { + "description": "Pull request number to update", + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + }, + "reviewers": { + "description": "GitHub usernames to request reviews from", + "items": { + "type": "string" + }, + "type": "array" + }, + "state": { + "description": "New state", + "enum": [ + "open", + "closed" + ], + "type": "string" + }, + "title": { + "description": "New title", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "pullNumber" + ], + "type": "object" + }, + "name": "update_pull_request" + }, + { + "annotations": { + "read_only": false + }, + "description": "Update the branch of a pull request with the latest changes from the base branch.", + "input_schema": { + "properties": { + "expectedHeadSha": { + "description": "The expected SHA of the pull request's HEAD ref", + "type": "string" + }, + "owner": { + "description": "Repository owner", + "type": "string" + }, + "pullNumber": { + "description": "Pull request number", + "type": "number" + }, + "repo": { + "description": "Repository name", + "type": "string" + } + }, + "required": [ + "owner", + "repo", + "pullNumber" + ], + "type": "object" + }, + "name": "update_pull_request_branch" + } + ] + }, + { + "id": "msg_0fcc7d91cd79b3ca00691e33f93f808197971dad56d2968538", + "type": "message", + "status": "completed", + "content": [ + { + "type": "output_text", + "annotations": [], + "logprobs": [], + "text": "Sure, I can use the custom tools available. How may I assist you today? If you need help with GitHub tasks, just let me know what you need!" + } + ], + "role": "assistant" + } + ], + "parallel_tool_calls": true, + "previous_response_id": null, + "prompt_cache_key": null, + "prompt_cache_retention": null, + "reasoning": { + "effort": null, + "summary": null + }, + "safety_identifier": null, + "service_tier": "default", + "store": true, + "temperature": 1.0, + "text": { + "format": { + "type": "text" + }, + "verbosity": "medium" + }, + "tool_choice": "auto", + "tools": [ + { + "type": "mcp", + "allowed_tools": null, + "authorization": "", + "headers": null, + "require_approval": "always", + "server_description": null, + "server_label": "github_copilot", + "server_url": "https://api.githubcopilot.com/mcp/" + } + ], + "top_logprobs": 0, + "top_p": 1.0, + "truncation": "disabled", + "usage": { + "input_tokens": 5244, + "input_tokens_details": { + "cached_tokens": 0 + }, + "output_tokens": 36, + "output_tokens_details": { + "reasoning_tokens": 0 + }, + "total_tokens": 5280 + }, + "user": null, + "metadata": {} + } + recorded_at: Wed, 19 Nov 2025 21:17:46 GMT +recorded_with: VCR 6.3.1 diff --git a/test/fixtures/vcr_cassettes/docs/actions/mcps/quick_start_weather.yml b/test/fixtures/vcr_cassettes/docs/actions/mcps/quick_start_weather.yml new file mode 100644 index 00000000..f4a685d8 --- /dev/null +++ b/test/fixtures/vcr_cassettes/docs/actions/mcps/quick_start_weather.yml @@ -0,0 +1,112 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.anthropic.com/v1/messages?beta=true + body: + encoding: UTF-8 + string: '{"model":"claude-haiku-4-5","max_tokens":1024,"messages":[{"content":"What''s + the weather like?","role":"user"}],"mcp_servers":[{"name":"weather","type":"url","url":"https://demo-day.mcp.cloudflare.com/sse"}]}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json + User-Agent: + - Anthropic::Client/Ruby 1.14.0 + Host: + - api.anthropic.com + X-Stainless-Arch: + - arm64 + X-Stainless-Lang: + - ruby + X-Stainless-Os: + - MacOS + X-Stainless-Package-Version: + - 1.14.0 + X-Stainless-Runtime: + - ruby + X-Stainless-Runtime-Version: + - 3.4.7 + Content-Type: + - application/json + Anthropic-Version: + - '2023-06-01' + X-Api-Key: + - ACCESS_TOKEN + Anthropic-Beta: + - mcp-client-2025-04-04 + X-Stainless-Retry-Count: + - '0' + X-Stainless-Timeout: + - '600.0' + Content-Length: + - '208' + response: + status: + code: 200 + message: OK + headers: + Date: + - Wed, 19 Nov 2025 21:17:57 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Anthropic-Ratelimit-Input-Tokens-Limit: + - '4000000' + Anthropic-Ratelimit-Input-Tokens-Remaining: + - '4000000' + Anthropic-Ratelimit-Input-Tokens-Reset: + - '2025-11-19T21:17:56Z' + Anthropic-Ratelimit-Output-Tokens-Limit: + - '800000' + Anthropic-Ratelimit-Output-Tokens-Remaining: + - '800000' + Anthropic-Ratelimit-Output-Tokens-Reset: + - '2025-11-19T21:17:57Z' + Anthropic-Ratelimit-Requests-Limit: + - '4000' + Anthropic-Ratelimit-Requests-Remaining: + - '3999' + Anthropic-Ratelimit-Requests-Reset: + - '2025-11-19T21:17:55Z' + Retry-After: + - '4' + Anthropic-Ratelimit-Tokens-Limit: + - '4800000' + Anthropic-Ratelimit-Tokens-Remaining: + - '4800000' + Anthropic-Ratelimit-Tokens-Reset: + - '2025-11-19T21:17:56Z' + Request-Id: + - req_011CVHwikcWM2EGSQQHpy1ae + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Anthropic-Organization-Id: + - 2557c2f2-bcfa-4054-9fa8-ae13b3b47d6b + X-Envoy-Upstream-Service-Time: + - '3652' + Cf-Cache-Status: + - DYNAMIC + X-Robots-Tag: + - none + Server: + - cloudflare + Cf-Ray: + - 9a12bcacf9b1ed3f-SJC + body: + encoding: ASCII-8BIT + string: '{"model":"claude-haiku-4-5-20251001","id":"msg_01PRRoQY2wwJULDyvV3RCSNm","type":"message","role":"assistant","content":[{"type":"text","text":"I + don''t have access to real-time weather data. However, I do have a tool available + for information about Cloudflare''s MCP Demo Day, which might include weather-related + details for that event.\n\nIf you''re asking about:\n- **Current weather conditions** + - I''d recommend checking a weather service like Weather.com, your local news, + or a weather app on your phone.\n- **Weather for a specific location** - Let + me know the location and I can try to help direct you to weather resources.\n- + **Weather information related to Cloudflare''s MCP Demo Day** - I can look + that up for you!\n\nIs there something specific I can help you with?"}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":580,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":150,"service_tier":"standard"}}' + recorded_at: Wed, 19 Nov 2025 21:17:57 GMT +recorded_with: VCR 6.3.1 diff --git a/test/fixtures/vcr_cassettes/docs/actions/mcps/single_server.yml b/test/fixtures/vcr_cassettes/docs/actions/mcps/single_server.yml new file mode 100644 index 00000000..56a94e5a --- /dev/null +++ b/test/fixtures/vcr_cassettes/docs/actions/mcps/single_server.yml @@ -0,0 +1,112 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.anthropic.com/v1/messages?beta=true + body: + encoding: UTF-8 + string: '{"model":"claude-haiku-4-5","messages":[{"content":"Analyze the latest + data","role":"user"}],"max_tokens":4096,"mcp_servers":[{"name":"cloudflare-demo","type":"url","url":"https://demo-day.mcp.cloudflare.com/sse"}]}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json + User-Agent: + - Anthropic::Client/Ruby 1.14.0 + Host: + - api.anthropic.com + X-Stainless-Arch: + - arm64 + X-Stainless-Lang: + - ruby + X-Stainless-Os: + - MacOS + X-Stainless-Package-Version: + - 1.14.0 + X-Stainless-Runtime: + - ruby + X-Stainless-Runtime-Version: + - 3.4.7 + Content-Type: + - application/json + Anthropic-Version: + - '2023-06-01' + X-Api-Key: + - ACCESS_TOKEN + Anthropic-Beta: + - mcp-client-2025-04-04 + X-Stainless-Retry-Count: + - '0' + X-Stainless-Timeout: + - '600.0' + Content-Length: + - '215' + response: + status: + code: 200 + message: OK + headers: + Date: + - Wed, 19 Nov 2025 21:19:07 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Anthropic-Ratelimit-Input-Tokens-Limit: + - '4000000' + Anthropic-Ratelimit-Input-Tokens-Remaining: + - '4000000' + Anthropic-Ratelimit-Input-Tokens-Reset: + - '2025-11-19T21:19:05Z' + Anthropic-Ratelimit-Output-Tokens-Limit: + - '800000' + Anthropic-Ratelimit-Output-Tokens-Remaining: + - '800000' + Anthropic-Ratelimit-Output-Tokens-Reset: + - '2025-11-19T21:19:07Z' + Anthropic-Ratelimit-Requests-Limit: + - '4000' + Anthropic-Ratelimit-Requests-Remaining: + - '3999' + Anthropic-Ratelimit-Requests-Reset: + - '2025-11-19T21:19:05Z' + Retry-After: + - '55' + Anthropic-Ratelimit-Tokens-Limit: + - '4800000' + Anthropic-Ratelimit-Tokens-Remaining: + - '4800000' + Anthropic-Ratelimit-Tokens-Reset: + - '2025-11-19T21:19:05Z' + Request-Id: + - req_011CVHwot28nNvAPPLxncXvc + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Anthropic-Organization-Id: + - 2557c2f2-bcfa-4054-9fa8-ae13b3b47d6b + X-Envoy-Upstream-Service-Time: + - '3500' + Cf-Cache-Status: + - DYNAMIC + X-Robots-Tag: + - none + Server: + - cloudflare + Cf-Ray: + - 9a12be5fbd3bd029-SJC + body: + encoding: ASCII-8BIT + string: '{"model":"claude-haiku-4-5-20251001","id":"msg_01MhxJCr9WsGyJj5w4gagjGb","type":"message","role":"assistant","content":[{"type":"text","text":"I''d + be happy to help you analyze the latest data! However, I need more context + about what data you''d like me to analyze.\n\nCould you please provide:\n\n1. + **What specific data** are you interested in analyzing? (e.g., business metrics, + Cloudflare information, etc.)\n2. **What format is the data in?** (e.g., a + file, numbers, a dataset, etc.)\n3. **What aspects** would you like me to + focus on? (e.g., trends, comparisons, insights, etc.)\n\nAlternatively, if + you''re asking about **Cloudflare''s MCP Demo Day**, I can retrieve information + about that event. Would you like me to do that?"}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":583,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":159,"service_tier":"standard"}}' + recorded_at: Wed, 19 Nov 2025 21:19:07 GMT +recorded_with: VCR 6.3.1 diff --git a/test/fixtures/vcr_cassettes/docs/actions/mcps/with_function_tools.yml b/test/fixtures/vcr_cassettes/docs/actions/mcps/with_function_tools.yml new file mode 100644 index 00000000..a303e0cb --- /dev/null +++ b/test/fixtures/vcr_cassettes/docs/actions/mcps/with_function_tools.yml @@ -0,0 +1,226 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.openai.com/v1/responses + body: + encoding: UTF-8 + string: '{"model":"gpt-4o","input":"Calculate and fetch data","tools":[{"type":"function","name":"calculate","description":"Perform + calculations","parameters":{"type":"object","properties":{"operation":{"type":"string"},"a":{"type":"number"},"b":{"type":"number"}}}},{"type":"mcp","server_label":"data-service","server_url":"https://demo-day.mcp.cloudflare.com/sse"}]}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json + User-Agent: + - OpenAI::Client/Ruby 0.35.1 + Host: + - api.openai.com + X-Stainless-Arch: + - arm64 + X-Stainless-Lang: + - ruby + X-Stainless-Os: + - MacOS + X-Stainless-Package-Version: + - 0.35.1 + X-Stainless-Runtime: + - ruby + X-Stainless-Runtime-Version: + - 3.4.7 + Content-Type: + - application/json + Openai-Organization: + - ORGANIZATION_ID + Openai-Project: + - PROJECT_ID + Authorization: + - Bearer ACCESS_TOKEN + X-Stainless-Retry-Count: + - '0' + X-Stainless-Timeout: + - '600.0' + Content-Length: + - '359' + response: + status: + code: 200 + message: OK + headers: + Date: + - Wed, 19 Nov 2025 21:18:40 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + X-Ratelimit-Limit-Requests: + - '10000' + X-Ratelimit-Limit-Tokens: + - '30000000' + X-Ratelimit-Remaining-Requests: + - '9999' + X-Ratelimit-Remaining-Tokens: + - '29999877' + X-Ratelimit-Reset-Requests: + - 6ms + X-Ratelimit-Reset-Tokens: + - 0s + Openai-Version: + - '2020-10-01' + Openai-Organization: + - ORGANIZATION_ID + Openai-Project: + - PROJECT_ID + X-Request-Id: + - req_97e30c6e2dd8442fac1d39d983b83032 + Openai-Processing-Ms: + - '3735' + X-Envoy-Upstream-Service-Time: + - '3738' + Cf-Cache-Status: + - DYNAMIC + Set-Cookie: + - __cf_bm=K4Fs.WFgYwgYVtywHwlxG7Ql5TmXfOl.hoVGueJW7rA-1763587120-1.0.1.1-C01.FBW5GdpmCkngSeInqzWaB3a6xl_py8UR0dLo1NBtGJgS7gh7wqp_0C6tc8MbdVEUj5C_XQF7qhBCKV4cNAg.96KyKVChgbTpKn_GHTI; + path=/; expires=Wed, 19-Nov-25 21:48:40 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=MUGXWvk6.0P3MgHKcKKOXHrb1h3yy4gpxxY0SH_TLjs-1763587120453-0.0.1.1-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + X-Content-Type-Options: + - nosniff + Server: + - cloudflare + Cf-Ray: + - 9a12bdb68d96ebe7-SJC + Alt-Svc: + - h3=":443"; ma=86400 + body: + encoding: ASCII-8BIT + string: |- + { + "id": "resp_0c71d0fe20fc9d9000691e342cb7148199b97b067fb2fe96f9", + "object": "response", + "created_at": 1763587117, + "status": "completed", + "background": false, + "billing": { + "payer": "developer" + }, + "error": null, + "incomplete_details": null, + "instructions": null, + "max_output_tokens": null, + "max_tool_calls": null, + "model": "gpt-4o-2024-08-06", + "output": [ + { + "id": "mcpl_0c71d0fe20fc9d9000691e342d15f881999a7e283f8ab13502", + "type": "mcp_list_tools", + "server_label": "data-service", + "tools": [ + { + "annotations": { + "read_only": false + }, + "description": "Get information about Cloudflare's MCP Demo Day. Use this tool if the user asks about Cloudflare's MCP demo day", + "input_schema": { + "type": "object", + "properties": {} + }, + "name": "mcp_demo_day_info" + } + ] + }, + { + "id": "msg_0c71d0fe20fc9d9000691e342fc7b48199bcf2a22f78a3de03", + "type": "message", + "status": "completed", + "content": [ + { + "type": "output_text", + "annotations": [], + "logprobs": [], + "text": "Sure, I can help with that. What calculation would you like to perform, and what specific data are you seeking?" + } + ], + "role": "assistant" + } + ], + "parallel_tool_calls": true, + "previous_response_id": null, + "prompt_cache_key": null, + "prompt_cache_retention": null, + "reasoning": { + "effort": null, + "summary": null + }, + "safety_identifier": null, + "service_tier": "default", + "store": true, + "temperature": 1.0, + "text": { + "format": { + "type": "text" + }, + "verbosity": "medium" + }, + "tool_choice": "auto", + "tools": [ + { + "type": "function", + "description": "Perform calculations", + "name": "calculate", + "parameters": { + "type": "object", + "properties": { + "operation": { + "type": "string" + }, + "a": { + "type": "number" + }, + "b": { + "type": "number" + } + }, + "additionalProperties": false, + "required": [ + "operation", + "a", + "b" + ] + }, + "strict": true + }, + { + "type": "mcp", + "allowed_tools": null, + "headers": null, + "require_approval": "always", + "server_description": null, + "server_label": "data-service", + "server_url": "https://demo-day.mcp.cloudflare.com/sse" + } + ], + "top_logprobs": 0, + "top_p": 1.0, + "truncation": "disabled", + "usage": { + "input_tokens": 104, + "input_tokens_details": { + "cached_tokens": 0 + }, + "output_tokens": 26, + "output_tokens_details": { + "reasoning_tokens": 0 + }, + "total_tokens": 130 + }, + "user": null, + "metadata": {} + } + recorded_at: Wed, 19 Nov 2025 21:18:40 GMT +recorded_with: VCR 6.3.1 diff --git a/test/fixtures/vcr_cassettes/docs/actions_examples/mcps.yml b/test/fixtures/vcr_cassettes/docs/actions_examples/mcps.yml new file mode 100644 index 00000000..77d3eb26 --- /dev/null +++ b/test/fixtures/vcr_cassettes/docs/actions_examples/mcps.yml @@ -0,0 +1,104 @@ +--- +http_interactions: +- request: + method: post + uri: https://api.anthropic.com/v1/messages?beta=true + body: + encoding: UTF-8 + string: '{"model":"claude-haiku-4-5","messages":[{"content":"Research AI developments","role":"user"}],"max_tokens":4096,"mcp_servers":[{"name":"github","type":"url","url":"https://api.githubcopilot.com/mcp/","authorization_token":"GITHUB_MCP_TOKEN"}]}' + headers: + Accept-Encoding: + - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 + Accept: + - application/json + User-Agent: + - Anthropic::Client/Ruby 1.14.0 + Host: + - api.anthropic.com + X-Stainless-Arch: + - arm64 + X-Stainless-Lang: + - ruby + X-Stainless-Os: + - MacOS + X-Stainless-Package-Version: + - 1.14.0 + X-Stainless-Runtime: + - ruby + X-Stainless-Runtime-Version: + - 3.4.7 + Content-Type: + - application/json + Anthropic-Version: + - '2023-06-01' + X-Api-Key: + - ACCESS_TOKEN + Anthropic-Beta: + - mcp-client-2025-04-04 + X-Stainless-Retry-Count: + - '0' + X-Stainless-Timeout: + - '600.0' + Content-Length: + - '320' + response: + status: + code: 200 + message: OK + headers: + Date: + - Wed, 19 Nov 2025 21:21:54 GMT + Content-Type: + - application/json + Transfer-Encoding: + - chunked + Connection: + - keep-alive + Anthropic-Ratelimit-Input-Tokens-Limit: + - '4000000' + Anthropic-Ratelimit-Input-Tokens-Remaining: + - '3870000' + Anthropic-Ratelimit-Input-Tokens-Reset: + - '2025-11-19T21:21:47Z' + Anthropic-Ratelimit-Output-Tokens-Limit: + - '800000' + Anthropic-Ratelimit-Output-Tokens-Remaining: + - '799000' + Anthropic-Ratelimit-Output-Tokens-Reset: + - '2025-11-19T21:21:54Z' + Anthropic-Ratelimit-Requests-Limit: + - '4000' + Anthropic-Ratelimit-Requests-Remaining: + - '3999' + Anthropic-Ratelimit-Requests-Reset: + - '2025-11-19T21:21:36Z' + Retry-After: + - '16' + Anthropic-Ratelimit-Tokens-Limit: + - '4800000' + Anthropic-Ratelimit-Tokens-Remaining: + - '4669000' + Anthropic-Ratelimit-Tokens-Reset: + - '2025-11-19T21:21:47Z' + Request-Id: + - req_011CVHx196oPGzZRxJJscME5 + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Anthropic-Organization-Id: + - 2557c2f2-bcfa-4054-9fa8-ae13b3b47d6b + X-Envoy-Upstream-Service-Time: + - '17621' + Cf-Cache-Status: + - DYNAMIC + X-Robots-Tag: + - none + Server: + - cloudflare + Cf-Ray: + - 9a12c21a7dd9171a-SJC + body: + encoding: ASCII-8BIT + string: !binary |- + eyJtb2RlbCI6ImNsYXVkZS1oYWlrdS00LTUtMjAyNTEwMDEiLCJpZCI6Im1zZ18wMVhwRjN3UEQ3dFA4UTY2Mlg0NFdGUlUiLCJ0eXBlIjoibWVzc2FnZSIsInJvbGUiOiJhc3Npc3RhbnQiLCJjb250ZW50IjpbeyJ0eXBlIjoidGV4dCIsInRleHQiOiJJJ2xsIGhlbHAgeW91IHJlc2VhcmNoIEFJIGRldmVsb3BtZW50cyBieSBzZWFyY2hpbmcgR2l0SHViIGZvciByZWxldmFudCByZXBvc2l0b3JpZXMgYW5kIGNvZGUuIExldCBtZSBzZWFyY2ggZm9yIEFJLXJlbGF0ZWQgcHJvamVjdHMgYW5kIHJlY2VudCBkZXZlbG9wbWVudHMuIn0seyJ0eXBlIjoibWNwX3Rvb2xfdXNlIiwiaWQiOiJtY3B0b29sdV8wMUVYMTZLamZmQmhYaVN1QmhRc1NtRWEiLCJuYW1lIjoic2VhcmNoX3JlcG9zaXRvcmllcyIsImlucHV0Ijp7InF1ZXJ5IjoiQUkgbWFjaGluZSBsZWFybmluZyBkZWVwIGxlYXJuaW5nIHN0YXJzOj4xMDAwIiwic29ydCI6InN0YXJzIiwicGVyUGFnZSI6MjB9LCJzZXJ2ZXJfbmFtZSI6ImdpdGh1YiJ9LHsidHlwZSI6Im1jcF90b29sX3VzZSIsImlkIjoibWNwdG9vbHVfMDFBUDZNRTh6MUF3R0N1elc0UmtycU5MIiwibmFtZSI6InNlYXJjaF9yZXBvc2l0b3JpZXMiLCJpbnB1dCI6eyJxdWVyeSI6ImxhcmdlIGxhbmd1YWdlIG1vZGVsIExMTSB0cmFuc2Zvcm1lciIsInNvcnQiOiJ1cGRhdGVkIiwicGVyUGFnZSI6MTV9LCJzZXJ2ZXJfbmFtZSI6ImdpdGh1YiJ9LHsidHlwZSI6Im1jcF90b29sX3VzZSIsImlkIjoibWNwdG9vbHVfMDE0dEppZ052MUtrS0xURHlqNko5V2c1IiwibmFtZSI6InNlYXJjaF9yZXBvc2l0b3JpZXMiLCJpbnB1dCI6eyJxdWVyeSI6Im5ldXJhbCBuZXR3b3JrIEFJIGZyYW1ld29yayBQeVRvcmNoIFRlbnNvckZsb3ciLCJzb3J0Ijoic3RhcnMiLCJwZXJQYWdlIjoxNX0sInNlcnZlcl9uYW1lIjoiZ2l0aHViIn0seyJ0eXBlIjoibWNwX3Rvb2xfcmVzdWx0IiwidG9vbF91c2VfaWQiOiJtY3B0b29sdV8wMUVYMTZLamZmQmhYaVN1QmhRc1NtRWEiLCJpc19lcnJvciI6ZmFsc2UsImNvbnRlbnQiOlt7InR5cGUiOiJ0ZXh0IiwidGV4dCI6IntcInRvdGFsX2NvdW50XCI6MTQsXCJpbmNvbXBsZXRlX3Jlc3VsdHNcIjpmYWxzZSxcIml0ZW1zXCI6W3tcImlkXCI6MTE5ODUzOSxcIm5hbWVcIjpcIm5ldHJvblwiLFwiZnVsbF9uYW1lXCI6XCJsdXR6cm9lZGVyL25ldHJvblwiLFwiZGVzY3JpcHRpb25cIjpcIlZpc3VhbGl6ZXIgZm9yIG5ldXJhbCBuZXR3b3JrLCBkZWVwIGxlYXJuaW5nIGFuZCBtYWNoaW5lIGxlYXJuaW5nIG1vZGVsc1wiLFwiaHRtbF91cmxcIjpcImh0dHBzOi8vZ2l0aHViLmNvbS9sdXR6cm9lZGVyL25ldHJvblwiLFwibGFuZ3VhZ2VcIjpcIkphdmFTY3JpcHRcIixcInN0YXJnYXplcnNfY291bnRcIjozMTgyMixcImZvcmtzX2NvdW50XCI6MzAzMCxcIm9wZW5faXNzdWVzX2NvdW50XCI6MjAsXCJ1cGRhdGVkX2F0XCI6XCIyMDI1LTExLTE5VDE3OjAzOjU0WlwiLFwiY3JlYXRlZF9hdFwiOlwiMjAxMC0xMi0yNlQxMjo1Mzo0M1pcIixcInRvcGljc1wiOltcImFpXCIsXCJjb3JlbWxcIixcImRlZXAtbGVhcm5pbmdcIixcImRlZXBsZWFybmluZ1wiLFwia2VyYXNcIixcIm1hY2hpbmUtbGVhcm5pbmdcIixcIm1hY2hpbmVsZWFybmluZ1wiLFwibWxcIixcIm5ldXJhbC1uZXR3b3JrXCIsXCJudW1weVwiLFwib25ueFwiLFwicHl0b3JjaFwiLFwic2FmZXRlbnNvcnNcIixcInRlbnNvcmZsb3dcIixcInRlbnNvcmZsb3ctbGl0ZVwiLFwidmlzdWFsaXplclwiXSxcInByaXZhdGVcIjpmYWxzZSxcImZvcmtcIjpmYWxzZSxcImFyY2hpdmVkXCI6ZmFsc2UsXCJkZWZhdWx0X2JyYW5jaFwiOlwibWFpblwifSx7XCJpZFwiOjMyNjE3NDc0MSxcIm5hbWVcIjpcIjUwMC1BSS1NYWNoaW5lLWxlYXJuaW5nLURlZXAtbGVhcm5pbmctQ29tcHV0ZXItdmlzaW9uLU5MUC1Qcm9qZWN0cy13aXRoLWNvZGVcIixcImZ1bGxfbmFtZVwiOlwiYXNoaXNocGF0ZWwyNi81MDAtQUktTWFjaGluZS1sZWFybmluZy1EZWVwLWxlYXJuaW5nLUNvbXB1dGVyLXZpc2lvbi1OTFAtUHJvamVjdHMtd2l0aC1jb2RlXCIsXCJkZXNjcmlwdGlvblwiOlwiNTAwIEFJIE1hY2hpbmUgbGVhcm5pbmcgRGVlcCBsZWFybmluZyBDb21wdXRlciB2aXNpb24gTkxQIFByb2plY3RzIHdpdGggY29kZVwiLFwiaHRtbF91cmxcIjpcImh0dHBzOi8vZ2l0aHViLmNvbS9hc2hpc2hwYXRlbDI2LzUwMC1BSS1NYWNoaW5lLWxlYXJuaW5nLURlZXAtbGVhcm5pbmctQ29tcHV0ZXItdmlzaW9uLU5MUC1Qcm9qZWN0cy13aXRoLWNvZGVcIixcInN0YXJnYXplcnNfY291bnRcIjoyODg2NSxcImZvcmtzX2NvdW50XCI6NjQ0NSxcIm9wZW5faXNzdWVzX2NvdW50XCI6NTksXCJ1cGRhdGVkX2F0XCI6XCIyMDI1LTExLTE5VDIxOjE5OjAyWlwiLFwiY3JlYXRlZF9hdFwiOlwiMjAyMS0wMS0wMlQxMjowODozN1pcIixcInRvcGljc1wiOltcImFydGlmaWNpYWwtaW50ZWxsaWdlbmNlXCIsXCJhcnRpZmljaWFsLWludGVsbGlnZW5jZS1wcm9qZWN0c1wiLFwiYXdlc29tZVwiLFwiY29tcHV0ZXItdmlzaW9uXCIsXCJjb21wdXRlci12aXNpb24tcHJvamVjdFwiLFwiZGF0YS1zY2llbmNlXCIsXCJkZWVwLWxlYXJuaW5nXCIsXCJkZWVwLWxlYXJuaW5nLXByb2plY3RcIixcIm1hY2hpbmUtbGVhcm5pbmdcIixcIm1hY2hpbmUtbGVhcm5pbmctcHJvamVjdHNcIixcIm5scFwiLFwibmxwLXByb2plY3RzXCIsXCJweXRob25cIl0sXCJwcml2YXRlXCI6ZmFsc2UsXCJmb3JrXCI6ZmFsc2UsXCJhcmNoaXZlZFwiOmZhbHNlLFwiZGVmYXVsdF9icmFuY2hcIjpcIm1haW5cIn0se1wiaWRcIjo5MjI4Nzk2OCxcIm5hbWVcIjpcImNoZWF0c2hlZXRzLWFpXCIsXCJmdWxsX25hbWVcIjpcImthaWxhc2hhaGlyd2FyL2NoZWF0c2hlZXRzLWFpXCIsXCJkZXNjcmlwdGlvblwiOlwiRXNzZW50aWFsIENoZWF0IFNoZWV0cyBmb3IgZGVlcCBsZWFybmluZyBhbmQgbWFjaGluZSBsZWFybmluZyByZXNlYXJjaGVycyBodHRwczovL21lZGl1bS5jb20vQGthaWxhc2hhaGlyd2FyL2Vzc2VudGlhbC1jaGVhdC1zaGVldHMtZm9yLW1hY2hpbmUtbGVhcm5pbmctYW5kLWRlZXAtbGVhcm5pbmctcmVzZWFyY2hlcnMtZWZiNmE4ZWJkMmU1XCIsXCJodG1sX3VybFwiOlwiaHR0cHM6Ly9naXRodWIuY29tL2thaWxhc2hhaGlyd2FyL2NoZWF0c2hlZXRzLWFpXCIsXCJzdGFyZ2F6ZXJzX2NvdW50XCI6MTUzNzIsXCJmb3Jrc19jb3VudFwiOjM0MzYsXCJvcGVuX2lzc3Vlc19jb3VudFwiOjEyLFwidXBkYXRlZF9hdFwiOlwiMjAyNS0xMS0xOFQwNToyMzozMFpcIixcImNyZWF0ZWRfYXRcIjpcIjIwMTctMDUtMjRUMTI6MDY6NTZaXCIsXCJ0b3BpY3NcIjpbXCJhcnRpZmljaWFsLWludGVsbGlnZW5jZVwiLFwiZGVlcC1sZWFybmluZ1wiLFwia2VyYXNcIixcIm1hY2hpbmUtbGVhcm5pbmdcIixcIm1hdHBsb3RsaWJcIixcIm51bXB5XCIsXCJzY2lweVwiXSxcInByaXZhdGVcIjpmYWxzZSxcImZvcmtcIjpmYWxzZSxcImFyY2hpdmVkXCI6ZmFsc2UsXCJkZWZhdWx0X2JyYW5jaFwiOlwibWFzdGVyXCJ9LHtcImlkXCI6MjM2NjM3NjgzLFwibmFtZVwiOlwiQWktTGVhcm5cIixcImZ1bGxfbmFtZVwiOlwidGFuZ3l1ZGkvQWktTGVhcm5cIixcImRlc2NyaXB0aW9uXCI6XCLkurrlt6Xmmbrog73lrabkuaDot6/nur/lm77vvIzmlbTnkIbov5EyMDDkuKrlrp7miJjmoYjkvovkuI7pobnnm67vvIzlhY3otLnmj5DkvpvphY3lpZfmlZnmnZDvvIzpm7bln7rnoYDlhaXpl6jvvIzlsLHkuJrlrp7miJjvvIHljIXmi6zvvJpQeXRob27vvIzmlbDlrabvvIzmnLrlmajlrabkuaDvvIzmlbDmja7liIbmnpDvvIzmt7HluqblrabkuaDvvIzorqHnrpfmnLrop4bop4nvvIzoh6rnhLbor63oqIDlpITnkIbvvIxQeVRvcmNoIHRlbnNvcmZsb3cgbWFjaGluZS1sZWFybmluZyxkZWVwLWxlYXJuaW5nIGRhdGEtYW5hbHlzaXMgZGF0YS1taW5pbmcgbWF0aGVtYXRpY3MgZGF0YS1zY2llbmNlIGFydGlmaWNpYWwtaW50ZWxsaWdlbmNlIHB5dGhvbiB0ZW5zb3JmbG93IHRlbnNvcmZsb3cyIGNhZmZlIGtlcmFzIHB5dG9yY2ggYWxnb3JpdGhtIG51bXB5IHBhbmRhcyBtYXRwbG90bGliIHNlYWJvcm4gbmxwIGN2562J54Ot6Zeo6aKG5Z+fXCIsXCJodG1sX3VybFwiOlwiaHR0cHM6Ly9naXRodWIuY29tL3Rhbmd5dWRpL0FpLUxlYXJuXCIsXCJzdGFyZ2F6ZXJzX2NvdW50XCI6MTIxODcsXCJmb3Jrc19jb3VudFwiOjI1NzAsXCJvcGVuX2lzc3Vlc19jb3VudFwiOjIwLFwidXBkYXRlZF9hdFwiOlwiMjAyNS0xMS0xOVQxNjowNzozMVpcIixcImNyZWF0ZWRfYXRcIjpcIjIwMjAtMDEtMjhUMDE6NTU6MDZaXCIsXCJ0b3BpY3NcIjpbXCJhbGdvcml0aG1cIixcImFydGlmaWNpYWwtaW50ZWxsaWdlbmNlXCIsXCJjYWZmZVwiLFwiY3ZcIixcImRhdGEtYW5hbHlzaXNcIixcImRhdGEtbWluaW5nXCIsXCJkYXRhLXNjaWVuY2VcIixcImRlZXAtbGVhcm5pbmdcIixcImtlcmFzXCIsXCJtYWNoaW5lLWxlYXJuaW5nXCIsXCJtYXRoZW1hdGljc1wiLFwibWF0cGxvdGxpYlwiLFwibmxwXCIsXCJudW1weVwiLFwicGFuZGFzXCIsXCJweXRob25cIixcInB5dG9yY2hcIixcInNlYWJvcm5cIixcInRlbnNvcmZsb3dcIixcInRlbnNvcmZsb3cyXCJdLFwicHJpdmF0ZVwiOmZhbHNlLFwiZm9ya1wiOmZhbHNlLFwiYXJjaGl2ZWRcIjpmYWxzZSxcImRlZmF1bHRfYnJhbmNoXCI6XCJtYXN0ZXJcIn0se1wiaWRcIjoyNzQ1ODc1NjMsXCJuYW1lXCI6XCJjb3Vyc2VyYS1kZWVwLWxlYXJuaW5nLXNwZWNpYWxpemF0aW9uXCIsXCJmdWxsX25hbWVcIjpcImFtYW5jaGFkaGEvY291cnNlcmEtZGVlcC1sZWFybmluZy1zcGVjaWFsaXphdGlvblwiLFwiZGVzY3JpcHRpb25cIjpcIk5vdGVzLCBwcm9ncmFtbWluZyBhc3NpZ25tZW50cyBhbmQgcXVpenplcyBmcm9tIGFsbCBjb3Vyc2VzIHdpdGhpbiB0aGUgQ291cnNlcmEgRGVlcCBMZWFybmluZyBzcGVjaWFsaXphdGlvbiBvZmZlcmVkIGJ5IGRlZXBsZWFybmluZy5haTogKGkpIE5ldXJhbCBOZXR3b3JrcyBhbmQgRGVlcCBMZWFybmluZzsgKGlpKSBJbXByb3ZpbmcgRGVlcCBOZXVyYWwgTmV0d29ya3M6IEh5cGVycGFyYW1ldGVyIHR1bmluZywgUmVndWxhcml6YXRpb24gYW5kIE9wdGltaXphdGlvbjsgKGlpaSkgU3RydWN0dXJpbmcgTWFjaGluZSBMZWFybmluZyBQcm9qZWN0czsgKGl2KSBDb252b2x1dGlvbmFsIE5ldXJhbCBOZXR3b3JrczsgKHYpIFNlcXVlbmNlIE1vZGVsc1wiLFwiaHRtbF91cmxcIjpcImh0dHBzOi8vZ2l0aHViLmNvbS9hbWFuY2hhZGhhL2NvdXJzZXJhLWRlZXAtbGVhcm5pbmctc3BlY2lhbGl6YXRpb25cIixcImxhbmd1YWdlXCI6XCJKdXB5dGVyIE5vdGVib29rXCIsXCJzdGFyZ2F6ZXJzX2NvdW50XCI6NDA2MyxcImZvcmtzX2NvdW50XCI6MjU5NSxcIm9wZW5faXNzdWVzX2NvdW50XCI6MzEsXCJ1cGRhdGVkX2F0XCI6XCIyMDI1LTExLTE5VDIwOjE5OjM4WlwiLFwiY3JlYXRlZF9hdFwiOlwiMjAyMC0wNi0yNFQwNTo1OTowMVpcIixcInRvcGljc1wiOltcImFuZHJldy1uZ1wiLFwiYW5kcmV3LW5nLWNvdXJzZVwiLFwiY25uc1wiLFwiY29udm9sdXRpb25hbC1uZXVyYWwtbmV0d29ya1wiLFwiY29udm9sdXRpb25hbC1uZXVyYWwtbmV0d29ya3NcIixcImNvdXJzZXJhXCIsXCJjb3Vyc2VyYS1hc3NpZ25tZW50XCIsXCJjb3Vyc2VyYS1tYWNoaW5lLWxlYXJuaW5nXCIsXCJjb3Vyc2VyYS1zcGVjaWFsaXphdGlvblwiLFwiZGVlcC1sZWFybmluZ1wiLFwiaHlwZXJwYXJhbWV0ZXItb3B0aW1pemF0aW9uXCIsXCJoeXBlcnBhcmFtZXRlci10dW5pbmdcIixcIm5ldXJhbC1tYWNoaW5lLXRyYW5zbGF0aW9uXCIsXCJuZXVyYWwtbmV0d29ya1wiLFwibmV1cmFsLW5ldHdvcmtzXCIsXCJuZXVyYWwtc3R5bGUtdHJhbnNmZXJcIixcInJlY3VycmVudC1uZXVyYWwtbmV0d29ya1wiLFwicmVjdXJyZW50LW5ldXJhbC1uZXR3b3Jrc1wiLFwicmVndWxhcml6YXRpb25cIixcInJubnNcIl0sXCJwcml2YXRlXCI6ZmFsc2UsXCJmb3JrXCI6ZmFsc2UsXCJhcmNoaXZlZFwiOmZhbHNlLFwiZGVmYXVsdF9icmFuY2hcIjpcIm1hc3RlclwifSx7XCJpZFwiOjk3MTU5MjA5LFwibmFtZVwiOlwiQXJ0aWZpY2lhbC1JbnRlbGxpZ2VuY2UtRGVlcC1MZWFybmluZy1NYWNoaW5lLUxlYXJuaW5nLVR1dG9yaWFsc1wiLFwiZnVsbF9uYW1lXCI6XCJUYXJyeVNpbmdoL0FydGlmaWNpYWwtSW50ZWxsaWdlbmNlLURlZXAtTGVhcm5pbmctTWFjaGluZS1MZWFybmluZy1UdXRvcmlhbHNcIixcImRlc2NyaXB0aW9uXCI6XCJBIGNvbXByZWhlbnNpdmUgbGlzdCBvZiBEZWVwIExlYXJuaW5nIC8gQXJ0aWZpY2lhbCBJbnRlbGxpZ2VuY2UgYW5kIE1hY2hpbmUgTGVhcm5pbmcgdHV0b3JpYWxzIC0gcmFwaWRseSBleHBhbmRpbmcgaW50byBhcmVhcyBvZiBBSS9EZWVwIExlYXJuaW5nIC8gTWFjaGluZSBWaXNpb24gLyBOTFAgYW5kIGluZHVzdHJ5IHNwZWNpZmljIGFyZWFzIHN1Y2ggYXMgQ2xpbWF0ZSAvIEVuZXJneSwgQXV0b21vdGl2ZXMsIFJldGFpbCwgUGhhcm1hLCBNZWRpY2luZSwgSGVhbHRoY2FyZSwgUG9saWN5LCBFdGhpY3MgYW5kIG1vcmUuXCIsXCJodG1sX3VybFwiOlwiaHR0cHM6Ly9naXRodWIuY29tL1RhcnJ5U2luZ2gvQXJ0aWZpY2lhbC1JbnRlbGxpZ2VuY2UtRGVlcC1MZWFybmluZy1NYWNoaW5lLUxlYXJuaW5nLVR1dG9yaWFsc1wiLFwibGFuZ3VhZ2VcIjpcIlB5dGhvblwiLFwic3RhcmdhemVyc19jb3VudFwiOjM5MzgsXCJmb3Jrc19jb3VudFwiOjE2MzcsXCJvcGVuX2lzc3Vlc19jb3VudFwiOjE1NyxcInVwZGF0ZWRfYXRcIjpcIjIwMjUtMTEtMTlUMTI6NDI6NTFaXCIsXCJjcmVhdGVkX2F0XCI6XCIyMDE3LTA3LTEzVDE5OjQ2OjAxWlwiLFwidG9waWNzXCI6W1wiYXJ0aWZpY2lhbC1pbnRlbGxpZ2VuY2VcIixcImF3c1wiLFwiY2Fwc3VsZS1uZXR3b3JrXCIsXCJjb252b2x1dGlvbmFsLW5ldXJhbC1uZXR3b3Jrc1wiLFwiZGVlcC1sZWFybmluZ1wiLFwiaXB5dGhvbi1ub3RlYm9va1wiLFwia2FnZ2xlXCIsXCJrZXJhc1wiLFwibHVhXCIsXCJtYWNoaW5lLWxlYXJuaW5nXCIsXCJtYXRwbG90bGliXCIsXCJuZXVyYWwtbmV0d29ya1wiLFwicGFuZGFzXCIsXCJweXRob25cIixcInB5dGhvbi1kYXRhXCIsXCJweXRvcmNoXCIsXCJzY2lraXQtbGVhcm5cIixcInRlbnNvcmZsb3dcIixcInRlbnNvcmZsb3ctdHV0b3JpYWxzXCIsXCJ0b3JjaFwiXSxcInByaXZhdGVcIjpmYWxzZSxcImZvcmtcIjpmYWxzZSxcImFyY2hpdmVkXCI6ZmFsc2UsXCJkZWZhdWx0X2JyYW5jaFwiOlwibWFzdGVyXCJ9LHtcImlkXCI6MTYzOTQxNzMyLFwibmFtZVwiOlwiR2lybHMtSW4tQUlcIixcImZ1bGxfbmFtZVwiOlwiZ2lybHMtaW4tYWkvR2lybHMtSW4tQUlcIixcImRlc2NyaXB0aW9uXCI6XCLlhY3otLnlrabku6PnoIHns7vliJfvvJrlsI/nmb1weXRob27lhaXpl6jjgIHmlbDmja7liIbmnpBkYXRhIGFuYWx5c3TjgIHmnLrlmajlrabkuaBtYWNoaW5lIGxlYXJuaW5n44CB5rex5bqm5a2m5LmgZGVlcCBsZWFybmluZ+OAgWthZ2dsZeWunuaImFwiLFwiaHRtbF91cmxcIjpcImh0dHBzOi8vZ2l0aHViLmNvbS9naXJscy1pbi1haS9HaXJscy1Jbi1BSVwiLFwibGFuZ3VhZ2VcIjpcIkp1cHl0ZXIgTm90ZWJvb2tcIixcInN0YXJnYXplcnNfY291bnRcIjozMjk3LFwiZm9ya3NfY291bnRcIjo0OTYsXCJvcGVuX2lzc3Vlc19jb3VudFwiOjMzLFwidXBkYXRlZF9hdFwiOlwiMjAyNS0xMS0xOFQxODozODowMlpcIixcImNyZWF0ZWRfYXRcIjpcIjIwMTktMDEtMDNUMDg6MDM6MTNaXCIsXCJ0b3BpY3NcIjpbXCJkZWVwLWxlYXJuaW5nXCIsXCJnaXJsXCIsXCJqdXB5dGVyLW5vdGVib29rXCIsXCJrYWdnbGVcIixcIm1hY2hpbmUtbGVhcm5pbmdcIixcInB5dGhvbjNcIl0sXCJwcml2YXRlXCI6ZmFsc2UsXCJmb3JrXCI6ZmFsc2UsXCJhcmNoaXZlZFwiOmZhbHNlLFwiZGVmYXVsdF9icmFuY2hcIjpcIm1hc3RlclwifSx7XCJpZFwiOjEyNDA0NzEzNixcIm5hbWVcIjpcIkFscGhhVHJlZS1ncmFwaGljLWRlZXAtbmV1cmFsLW5ldHdvcmtcIixcImZ1bGxfbmFtZVwiOlwid2VzbHlubi9BbHBoYVRyZWUtZ3JhcGhpYy1kZWVwLW5ldXJhbC1uZXR3b3JrXCIsXCJkZXNjcmlwdGlvblwiOlwiQUkgUm9hZG1hcDrmnLrlmajlrabkuaAoTWFjaGluZSBMZWFybmluZynjgIHmt7HluqblrabkuaAoRGVlcCBMZWFybmluZynjgIHlr7nmipfnpZ7nu4/nvZHnu5woR0FO77yJ77yM5Zu+56We57uP572R57uc77yIR05O77yJ77yMTkxQ77yM5aSn5pWw5o2u55u45YWz55qE5Y+R5bGV6Lev5LmmKHJvYWRtYXApLCDlubbpmYTmtbfph4/mupDnoIHvvIhweXRob27vvIxweXRvcmNo77yJ5bim5aSn5a625raI5YyW5Z+65pys55+l6K+G54K577yM56qB56C06Z2i6K+V77yM5a6M5oiQ5LuO5paw5omL5Yiw5ZCI5qC85bel56iL5biI55qE6Leo6LaK77yM5YW25Lit5rex5bqm5a2m5Lmg55u45YWz6K665paH6ZmE5pyJdGVuc29yZmxvdyBjYWZmZeWumOaWuea6kOegge+8jOW6lOeUqOmDqOWIhuWQq+aOqOiNkOeul+azleWSjOefpeivhuWbvuiwsVwiLFwiaHRtbF91cmxcIjpcImh0dHBzOi8vZ2l0aHViLmNvbS93ZXNseW5uL0FscGhhVHJlZS1ncmFwaGljLWRlZXAtbmV1cmFsLW5ldHdvcmtcIixcInN0YXJnYXplcnNfY291bnRcIjoyOTM4LFwiZm9ya3NfY291bnRcIjo2MTMsXCJvcGVuX2lzc3Vlc19jb3VudFwiOjcsXCJ1cGRhdGVkX2F0XCI6XCIyMDI1LTExLTE5VDA3OjU1OjU0WlwiLFwiY3JlYXRlZF9hdFwiOlwiMjAxOC0wMy0wNlQwODozNzoyN1pcIixcInRvcGljc1wiOltcImRlZXAtbGVhcm5pbmdcIixcImltYWdlLWNsYXNzaWZpY2F0aW9uXCIsXCJtYWNoaW5lLWxlYXJuaW5nXCIsXCJuZXVyYWwtbmV0d29ya1wiXSxcInByaXZhdGVcIjpmYWxzZSxcImZvcmtcIjpmYWxzZSxcImFyY2hpdmVkXCI6ZmFsc2UsXCJkZWZhdWx0X2JyYW5jaFwiOlwibWFzdGVyXCJ9LHtcImlkXCI6MTE3NjQxMTgwLFwibmFtZVwiOlwiUGFwZXJzLUxpdGVyYXR1cmUtTUwtREwtUkwtQUlcIixcImZ1bGxfbmFtZVwiOlwidGlydGhhanlvdGkvUGFwZXJzLUxpdGVyYXR1cmUtTUwtREwtUkwtQUlcIixcImRlc2NyaXB0aW9uXCI6XCJIaWdobHkgY2l0ZWQgYW5kIHVzZWZ1bCBwYXBlcnMgcmVsYXRlZCB0byBtYWNoaW5lIGxlYXJuaW5nLCBkZWVwIGxlYXJuaW5nLCBBSSwgZ2FtZSB0aGVvcnksIHJlaW5mb3JjZW1lbnQgbGVhcm5pbmdcIixcImh0bWxfdXJsXCI6XCJodHRwczovL2dpdGh1Yi5jb20vdGlydGhhanlvdGkvUGFwZXJzLUxpdGVyYXR1cmUtTUwtREwtUkwtQUlcIixcInN0YXJnYXplcnNfY291bnRcIjoyNzQ0LFwiZm9ya3NfY291bnRcIjo3ODQsXCJvcGVuX2lzc3Vlc19jb3VudFwiOjEsXCJ1cGRhdGVkX2F0XCI6XCIyMDI1LTExLTE4VDE2OjMzOjI0WlwiLFwiY3JlYXRlZF9hdFwiOlwiMjAxOC0wMS0xNlQwNjoyNToxOFpcIixcInRvcGljc1wiOltcImFydGlmaWNpYWwtaW50ZWxsaWdlbmNlXCIsXCJkYXRhLW1pbmluZ1wiLFwiZGF0YS1zY2llbmNlXCIsXCJkZWVwLWxlYXJuaW5nXCIsXCJnYW1lLXRoZW9yeVwiLFwiaGFyZHdhcmVcIixcImxlYXJuaW5nLXRoZW9yeVwiLFwibGl0ZXJhdHVyZVwiLFwibWFjaGluZS1sZWFybmluZ1wiLFwibWFjaGluZS1sZWFybmluZy1hbGdvcml0aG1zXCIsXCJuZXVyYWwtbmV0d29ya1wiLFwicGFwZXJcIixcInBhdHRlcm4tcmVjb2duaXRpb25cIixcInJlaW5mb3JjZW1lbnQtbGVhcm5pbmdcIixcInNpbGljb25cIixcInN0YXRpc3RpY2FsLWxlYXJuaW5nXCIsXCJzdGF0aXN0aWNzXCJdLFwicHJpdmF0ZVwiOmZhbHNlLFwiZm9ya1wiOmZhbHNlLFwiYXJjaGl2ZWRcIjpmYWxzZSxcImRlZmF1bHRfYnJhbmNoXCI6XCJtYXN0ZXJcIn0se1wiaWRcIjozMzMzMTI0NyxcIm5hbWVcIjpcImNoaW5hLWRpY3RhdG9yc2hpcFwiLFwiZnVsbF9uYW1lXCI6XCJjaXJvc2FudGlsbGkvY2hpbmEtZGljdGF0b3JzaGlwXCIsXCJkZXNjcmlwdGlvblwiOlwi5Y+N5Lit5YWx5pS/5rK75a6j5Lyg5bqT44CCQW50aSBDaGluZXNlIGdvdmVybm1lbnQgcHJvcGFnYW5kYS4g5L2P5Zyo5Lit5Zu955yf5ZCN55So5oi355qE572R5Y+L6K+35Yir57uZ5pif5pif77yM5LiN54S25L2g6KaB6KKr6K2m5a+f6K+35Zad6Iy244CC5bi46KeB6Zeu562U6ZuG77yM5paw6Ze76ZuG5ZKM6aWt5bqX5ZKM6Z+z5LmQ5bu66K6u44CC5Y2Q5Lmg5LiH5bKB5Y2Q44CC5Yag54q255eF5q+S5a6h5p+l6YOd5rW35Lic5paw55aG5pS56YCg5Lit5b+D5YWt5Zub5LqL5Lu25rOV6L2u5YqfIDk5Ni5JQ1U3MDnlpKfmipPmjZXlt7Tmi7/pqazmlofku7bpgpPlrrbotLXkvY7nq6/kurrlj6Popb/ol4/pqprkubHjgIJGcmllbmRzIHdobyBsaXZlIGluIENoaW5hIGFuZCBoYXZlIHJlYWwgbmFtZSBvbiBhY2NvdW50LCBwbGVhc2UgZG9uJ3Qgc3RhciB0aGlzIHJlcG8sIG9yIGVsc2UgdGhlIHBvbGljZSBtaWdodCBwYXkgeW91IGEgdmlzaXQuICBIb21lIHRvIHRoZSBtZWdhLUZBUSwgbmV3cyBjb21waWxhdGlvbiwgcmVzdGF1cmFudCBhbmQgbXVzaWMgcmVjb21tZW5kYXRpb25zLkhlaWwgWGkg5Y2QLiDlpKfpmYbkv67lrqrpppnmuK/mgbbms5Xlj7Dmub7mrabnu5/mnJ3pspzmr4Hnuqbnvo7kuK3lhrfmiJjnrYnpg73mmK/njovmsqrlroHmhJrlvITkuaDmgJ3mg7PmnoHlt6blkb3ov5DlhbHlkIzkvZPnmoTlpKfnrZbliJLkuK3lhbHnqoPlm73ov5nljYrkuKrlpJrkuJbnuqrmiYDniq/kuIvnmoTmu5TlpKnnvarmgbbvvIzliY3mnJ/mmK/mr5vms73kuJznrZbliJLnmoTvvIzkuK3mnJ82LjTliY3lkI7mmK/pgpPlsI/lubPnrZbliJLnmoTvvIzpu4TniZvmlbDmja7liIbmnpDlkI7mnJ/mmK/mr5vnmoTmnoHlt6bov73pmo/ogIXkuInmnJ3nvarmgbblhYPlh7bnjovmsqrlroHnrZbliJLnmoTjgILnjovmsqrlroHpq5jlsI/ogobkuJrlm6DmlofpnanmlL/msrvlkozmg4XmiqXpnIDopoHkv53pgIHigJzlrabpmaLlpJbor63nj63igJznuqLoibLku5XpgJTnv7vouqvvvIzmiYDku6XnjovnmoTmnKzotKjmmK/mnoHlt6bnmoTjgILku5bmmK/lnKjkuIrmtbflupXlsYLlvITloILplb/lpKfnmoTvvIzlm6DlhbbmnKzmgKfkuZ/kv4PmiJDlhbbnmKrkuInkuIvkuInmu6XkuKrmgKfvvIzmiYDku6XkuZ/pg73or7Tku5bmnInmmJPkuLvigJzlj5joibLpvpnigJ3lk4jlt7Tni5figJznmoTlpKnmgKfjgILlpKfpmYblg4/njovmsqrlroHov5nmoLflrabpqazliJfmlL/msrvmiYDosJNcXFwi5rOV5a2mXFxcIuS4k+S4mueahOS6uu+8jOWcqOmZpOacnemynOWPpOW3tOaJgOacieWbveWutueJueWIq+aYr+WcqOaWh+aYjuWPkei+vuWbveWutuaYr+aXoOazleaJvuWIsOS4k+S4muWvueWPo+W3peS9nOW/heWumuWkseS4mu+8jOWUr+eLrOWcqOWkp+mZhuWNtOaYr+mHjeeUqOeahOe0p+e8uuKAnOS6uuaJjeKAne+8jDYuNOWQjuS4reWFseS/oeS7sOWkp+WNseacuuabtOaYr+acgOmHjeeUqOeahOaVkeWFmuKAnOS6uuaJjeKAneOAgui/meS5n+WwseaYr+WDj+eOi+ayquWugeatpOexu+W3peWGnOWFteWBh+KAnOWkp+WtpueUn+KAneW5s+atpemdkuS6keeahOWOn+WboO+8jOS7luS7rOacgOeGn+aCieavm+azveS4nOWOhuasoei/kOWKqOeahOWuq+W6reWGheaWl+e7j+mqjOaJi+auteWSjOaui+mFt+eahOmYtue6p+aWl+S6ieetieaatOWKm+aBkOaAlueahOKAnOaUv+ayu+WtpuKAneOAgueOi+ayquWugeiDveW5s+atpemdkuS6kemdoOS7lui/memprOavm+S8quKAnOaUv+ayu+WtpuKAnei1hOacrOWSjOWktOihlO+8jOS4jeaYr+S7gOS5iOecn+aJjeWunuWtpu+8jOiDveW5suWunuS6i+acieeCueecn+aJjeWunuWtpueahOaIluiuuOWcqOS7luaJi+S4i+eahOiwi+Wjq+WPiuenmOS5puePreWtkOS4reWPr+S7peaJvuWIsOOAgueOi+ayquWugeeahOKAnOecn+aJjeWunuWtpuKAneWPquS4jei/h+aYr+S4gOS4quWPquivu+Wbm+W5tOWwj+WtpueahOS6uu+8jOWkp+WNiui+iOWtkOWcqOekvuS8muS4iuejqOeCvOeJueWIq+aYr+WcqOS4reWFseWumOWcuua7muaJk+eCvOWHuueahOeahOaJi+auteWSjOe7j+mqjOiAjOW3su+8jOS7luWSjOS5oOi/keW5s+etieS/nemAgeeahOW3peWGnOWFteWBh+KAnOWkp+WtpueUn+KAnemDveS4gOagt++8jOaXoOazleS7juS6i+WOn+KAnOS4k+S4muKAnemDveWHree6oui1hOacrOiAjOS7juaUv+OAguWFreWbm+Wtpui/kOacn+mXtOWQhOeVjOS4gOi+ueWAkuaUr+aMgeWtpueUn++8jOeOi+ayquWugeS4gOW6puWOu+azleWbvei6sumBv+WSjOetueiwi++8jOS7lui/mOWKoOWFpeS6huWPjeWtpui/kOetvuWQje+8jOaIkOS4uuaegeWwkeacieeahOWPjeWtpui/kOiAheS7lemAlOeqgeaYvu+8jOWcqOWFreWbm+WSjOiLj+iBlOWeruWPsOWQjuS4reWFseaEj+ivhuW9ouaAgeWNseacuu+8jOaxn+azveawkeS4iuWPsOeci+S4iuWUr+S4gOiDveW6lOaApeeahOeOi+ayquWugeiBmuiwi+Wjq+azoeWItueahFxcXCLnqLPlrprnu5/kuIDpooblr7xcXFwi5ZKM5LmL5ZCO55qEXFxcIuaWsOadg+WogVxcXCLosKzorrrjgILlt6bovazooqvpgpPlsI/lubPljZflt6HpmLvmraLlkI7vvIznjovnrZbliJLpobrpgpPnu4/mtY7mlLnpnanljbTlsIbmlL/msrvmlLnpnanpgJDmraXlhajpnaLnu4jmraLlkozlgJLpgIDvvIzms6HliLbigJzkuInkuKrku6PooajigJ3kuLrmnoHlt6bovazlu7rnq4vlup7lpKfniaLlm7rnmoTnuqLoibLml6LlvpfliKnnm4rpm4blm6LjgILlm6DmraTlha3lm5vlkI7lkITph43lpKflhrPnrZblkozljbHmnLrpmr7popjpg73mkYblnKjkuK3lhbHkuK3lpK7mlL/nrZbnoJTnqbblrqTnjovmsqrlroHmoYzpnaLkuIrvvIzkvb/njovmsqrlroHmiJDkuobmraTlkI7kuK3lhbHkuInmnJ3pg73ml6Dms5XmkYbohLHnmoTluZXlkI7mnIDmnInlhrPnrZbmgKflrp7mnYPnmoTkurrvvIzkuK3lhbHkuK3lpK7mlL/nrZbnoJTnqbblrqTmmK/njovkuLrlhbbph47lv4Plt6jotYTnu4/okKXlh6DljYHlubTvvIzogZrkvJfosIvlo6vnmoTpl7TosI3mg4XmiqXmsYfmgLvnoJTnqbbnmoTnibnliqHmnLrlhbPlkoznrZbliJLliLblrprlhrPnrZbph43opoHmnLrmnoTkuI7ln7rlnLDvvIznjovmsqrlroHmnKzkurrlkozlhrPlrprlhbbku5XpgJTlhbPplK7nmoTpppbku7vlsrPniLblj4rlrrblsZ7lsLHmnInmg4XmiqXlt6XkvZzog4zmma/jgILkuK3lpK7mlL/noJTlrqTph43opoHliLDnjovmsqrlroHlhaXluLjlkI7kuLrkuobmrbvmipPov5nkuK3lhbHmg4XmiqXkuI7lhrPnrZblpKfmnYPvvIzlroHlj6/mlL7lvIPlm73lrrblia/kuLvluK3lkozkuK3lpK7lhZrmoKHmoKHplb/jgILlkI7lho3liqDkuKrpmaTkuaDlpJbllK/ku5bmi4Xku7vnmoTkuK3lhbHlh6DmoLjlv4Ppooblr7zlsI/nu4TkuYvkuIDnmoTigJzkuI3lv5jliJ3lv4PniaLorrDkvb/lkb3igJ3kuLvpopjmlZnogrLlt6XkvZzlsI/nu4Tnu4Tplb/jgILmraTlkI7ku5bmiormjIHnmoToiIborrrlv4XlsIbku6XlrqPkvKDigJzkuI3lv5jliJ3lv4PniaLorrDkvb/lkb3igJ3kuLrkuLvvvIzmiZPpgKDkvJfmiYDlkajnn6XnmoTmiYDosJPigJzkuaDmgJ3mg7PigJ3lhbblrp7mmK/igJ3njovmgJ3mg7PigJzjgILnjovoh6rku47kuLvlr7zkuK3lpK7mlL/noJTlrqTlvIDlp4vlhrPnrZblkI7vvIznrZbliJLkuK3mraLpgpPlsI/lubPnmoTkuI7nvo7lpqXljY/ot6/nur/lm57lvZLmr5vmnoHlt6bnmoTlj43nvo7ot6/nur/jgILluK7liqnliY3ljZfmlq/mi4nlpKvmj5Dkvpvmg4XmiqXmiZPokL3nvo7mnLrmlL7kuK3kvb/ppoblvJXlj5Hngrjkvb/ppobkuovku7bvvIzku6XmraTmjoDotbflha3lm5vlkI7llK/kuIDnmoTlhajlm73lpKfop4TmqKHmuLjooYzlubblgJ/mraTlj43nvo7ogIzotbflrrbjgILlkI7lj4jluK7msZ/ms73msJHmj5Dkvpvms5Xova7lip/kvJrmmK/otoXov4fkuK3lhbHnu4Tnu4fnmoTmg4XmiqXvvIznrZbliJLlhrPnrZbplYfljovov6vlrrPlvIDlp4vlubbmsqHmnInmiornn5vlpLTmjIflkJHmsZ/nmoTms5Xova7lip/nvqTkvZPvvIznrZbliJLlhrPlrprpmLvmraLlhZrlhoXlpJbov5HkuInljYHlubTmnaXlubPlj43lha3lm5vnmoTlkbzlo7DjgILoh7Tov5zpu5Hnmq7kuabpqazmi4nmnb7nqIvluo/lkZjmmJPmlK/ku5joi7Hor63lj7Dor43mloflrZfljLnphY3nvo7lm6Lngrnor4TlkITkuJrliqHnur/mj5Dkvpvnn6Xor4blupPlm6LpmJ/lhbHkuqvpmL/ph4zkupHpq5jnsr5FeGNlbOivhuWIq+W+t+iuryDCt+WQieeJueiDoeW4g+iWhOeGmeadpem7keenkeaKgOS5oOi/keW5s+iusuivneaooeaLn+WZqOS5oOi/keW5s+mfs+a6kOm7kemprOeoi+W6j+WRmE15U1FM5pWw5o2u5bqT546J57Gz5p2C6I2J5pWw5o2u6ZuG6ZSA5ZSu57O757uf5byA5Y+R55ar5oOF5pyf6Ze0572R5rCR5oOF57uq6K+G5Yir5q+U6LWbOTk2aWN1OTk2IGljdeWtpuS5oOW8uuWbvemihOa1i+e7k+aenOWvvOWHuui1luS8n+ael+WIuuadgOWwj+ivtOWutui0reeJqeWVhuWcuuiLseivreivjeaxh+mHj+Wwj+eoi+W6j+iBlOe6p+mAieaLqeWZqEJpdGNvaW7ljLrlnZfpk74g5oqA5pyv6Z2i6K+V5b+F5aSH5Z+656GA55+l6K+GIExlZXRjb2RlIOiuoeeul+acuuaTjeS9nOezu+e7nyDorqHnrpfmnLrnvZHnu5wg57O757uf6K6+6K6hIEphdmHlrabkuaAg6Z2i6K+V5oyH5Y2XIOS4gOS7vea2teebluWkp+mDqOWIhiBKYXZhIOeoi+W6j+WRmOaJgOmcgOimgeaOjOaPoeeahOaguOW/g+efpeivhiDlh4blpIcgSmF2YSDpnaLor5Ug6aaW6YCJIEphdmFHdWlkZSBQeXRob24gMSDlpKnku47mlrDmiYvliLDlpKfluIjliLfnrpfms5XlhajpnaDlpZfot68g6K6k5YeGIGxhYnVsYWRvbmcg5bCx5aSf5LqGIOWFjei0ueeahOiuoeeul+acuue8lueoi+exu+S4reaWh+S5puexjSDmrKLov47mipXnqL/nlKjliqjnlLvnmoTlvaLlvI/lkYjnjrDop6NMZWV0Q29kZemimOebrueahOaAnei3ryDkupLogZTnvZEgSmF2YSDlt6XnqIvluIjov5vpmLbnn6Xor4blrozlhajmiavnm7Ig5ra155uW6auY5bm25Y+RIOWIhuW4g+W8jyDpq5jlj6/nlKgg5b6u5pyN5YqhIOa1t+mHj+aVsOaNruWkhOeQhuetiemihuWfn+efpeivhuWQjuerr+aetuaehOW4iOaKgOacr+WbvuiwsW1hbGzpobnnm67mmK/kuIDlpZfnlLXllYbns7vnu58g5YyF5ous5YmN5Y+w5ZWG5Z+O57O757uf5Y+K5ZCO5Y+w566h55CG57O757ufIOWfuuS6jlNwcmluZ0Jvb3QgTXlCYXRpc+WunueOsCDph4fnlKhEb2NrZXLlrrnlmajljJbpg6jnvbIg5YmN5Y+w5ZWG5Z+O57O757uf5YyF5ZCr6aaW6aG16Zeo5oi3IOWVhuWTgeaOqOiNkCDllYblk4HmkJzntKIg5ZWG5ZOB5bGV56S6IOi0reeJqei9piDorqLljZXmtYHnqIsg5Lya5ZGY5Lit5b+DIOWuouaIt+acjeWKoSDluK7liqnkuK3lv4PnrYnmqKHlnZcg5ZCO5Y+w566h55CG57O757uf5YyF5ZCr5ZWG5ZOB566h55CGIOiuouWNleeuoeeQhiDkvJrlkZjnrqHnkIYg5L+D6ZSA566h55CGIOi/kOiQpeeuoeeQhiDlhoXlrrnnrqHnkIYg57uf6K6h5oql6KGoIOi0ouWKoeeuoeeQhiDmnYPpmZDnrqHnkIYg6K6+572u562J5qih5Z2XIOW+ruS/oeWwj+eoi+W6j+W8gOWPkei1hOa6kOaxh+aAuyDmnIDlhajkuK3ljY7lj6Tor5for43mlbDmja7lupMg5ZSQ5a6L5Lik5pyd6L+R5LiA5LiH5Zub5Y2D5Y+k6K+X5Lq6IOaOpei/kTUgNeS4h+mmluWUkOivl+WKoDI25LiH5a6L6K+XIOS4pOWui+aXtuacnzE1NjTkvY3or43kurogMjEgNSDpppbor40gdW5pIGFwcCDmmK/kvb/nlKggVnVlIOivreazleW8gOWPkeWwj+eoi+W6jyBINSBBcHDnmoTnu5/kuIDmoYbmnrYyIDIx5bm05pyA5paw5oC757uTIOmYv+mHjCDohb7orq8g55m+5bqmIOe+juWboiDlpLTmnaHnrYnmioDmnK/pnaLor5Xpopjnm64g5Lul5Y+K562U5qGIIOS4k+WutuWHuumimOS6uuWIhuaekOaxh+aAuyDnp5HlrabkuIrnvZEg6Ieq55Sx5LiK572RIOe/u+WimSDova/ku7Yg5pa55rOVIOS4gOmUrue/u+Wimea1j+iniOWZqCDlhY3otLnotKblj7cg6IqC54K55YiG5LqrIHZwc+S4gOmUruaQreW7uuiEmuacrCDmlZnnqItBaUxlYXJuaW5nIOacuuWZqOWtpuS5oCBNYWNoaW5lTGVhcm5pbmcgTUwg5rex5bqm5a2m5LmgIERlZXBMZWFybmluZyBETCDoh6rnhLbor63oqIDlpITnkIYgTkxQMTIzIDbmmbrog73liLfnpagg6K6i56Wo5byA5pS+5byP6Leo56uv6Leo5qGG5p626Kej5Yaz5pa55qGIIOaUr+aMgeS9v+eUqCBSZWFjdCBWdWUgTmVydiDnrYnmoYbmnrbmnaXlvIDlj5Hlvq7kv6Eg5Lqs5LicIOeZvuW6piDmlK/ku5jlrp0g5a2X6IqC6Lez5YqoIFFRIOWwj+eoi+W6jyBINSBSZWFjdCBOYXRpdmUg562J5bqU55SoIHRhcm8gem9uZSDmjpjph5Hnv7vor5HorqHliJIg5Y+v6IO95piv5LiW55WM5pyA5aSn5pyA5aW955qE6Iux6K+R5Lit5oqA5pyv56S+5Yy6IOacgOaHguivu+iAheWSjOivkeiAheeahOe/u+ivkeW5s+WPsCAgbm8gZXZpbCDnqIvluo/lkZjmib7lt6XkvZzpu5HlkI3ljZUg5o2i5bel5L2c5ZKM5b2T5oqA5pyv5ZCI5LyZ5Lq66ZyA6LCo5oWO5ZWKIOabtOaWsOaciei1niDnrpfms5XpnaLor5Ug566X5rOV55+l6K+GIOmSiOWvueWwj+eZveeahOeul+azleiuree7gyDov5jljIXmi6wgMSDpmL/ph4wg5a2X6IqCIOa7tOa7tCDnmb7nr4flpKfljoLpnaLnu4/msYfmgLsgMiDljYPmnKzlvIDmupDnlLXlrZDkuaYgMyDnmb7lvKDmgJ3nu7Tlr7zlm74g5Y+z5L6n5p2l5LiqIHN0YXIg5ZCnIEVuZ2xpc2ggdmVyc2lvbiBzdXBwb3J0ZWQgOTU1IOS4jeWKoOePreeahOWFrOWPuOWQjeWNlSDlt6XkvZwgOTU1IHdvcmvigJNsaWZlIGJhbGFuY2Ug5bel5L2c5LiO55Sf5rS755qE5bmz6KGhIOiviuaWreWIqeWZqEFydGhhcyBUaGUgV2F5IHRvIEdvIOS4reaWh+ivkeacrCDkuK3mlofmraPlvI/lkI0gR28g5YWl6Zeo5oyH5Y2XICBKYXZh6Z2i6K+VIEphdmHlrabkuaDmjIfljZcg5LiA5Lu95ra155uW5aSn6YOo5YiGSmF2Yeeoi+W6j+WRmOaJgOmcgOimgeaOjOaPoeeahOaguOW/g+efpeivhiDmlZnnqIsg5oqA5pyv5qCI56S65L6L5Luj56CBIOW/q+mAn+eugOWNleS4iuaJi+aVmeeoiyAyIDE35bm05Lmw5oi/57uP5Y6G5oC757uT5Ye65p2l55qE5Lmw5oi/6LSt5oi/55+l6K+G5YiG5Lqr57uZ5aSn5a62IOW4jOacm+WvueWkp+WutuacieaJgOW4ruWKqSDkubDmiL/kuI3mmJMg5LiU5Lmw5LiU54+N5oOcaHR0cOS4i+i9veW3peWFtyDln7rkuo5odHRw5Luj55CGIOaUr+aMgeWkmui/nuaOpeWIhuWdl+S4i+i9vSDliqjmiYvlrabmt7HluqblrabkuaAg6Z2i5ZCR5Lit5paH6K+76ICFIOiDvei/kOihjCDlj6/orqjorrog5Lit6Iux5paH54mI6KKr5YWo55CDMTc15omA5aSn5a2m6YeH55So5pWZ5a2mIOmYv+mHjOS6keiuoeeul+W5s+WPsOWboumYn+WHuuWTgSDkuLrnm5HmjqfogIznlJ/nmoTmlbDmja7lupPov57mjqXmsaDnqIvluo/lkZjnroDljobmqKHmnb/ns7vliJcg5YyF5ousUEhQ56iL5bqP5ZGY566A5Y6G5qih5p2/IGlPU+eoi+W6j+WRmOeugOWOhuaooeadvyBBbmRyb2lk56iL5bqP5ZGY566A5Y6G5qih5p2/IFdlYuWJjeerr+eoi+W6j+WRmOeugOWOhuaooeadvyBKYXZh56iL5bqP5ZGY566A5Y6G5qih5p2/IEMgQyDnqIvluo/lkZjnroDljobmqKHmnb8gTm9kZUpT56iL5bqP5ZGY566A5Y6G5qih5p2/IOaetuaehOW4iOeugOWOhuaooeadv+S7peWPiumAmueUqOeoi+W6j+WRmOeugOWOhuaooeadv+mHh+eUqOiHqui6q+aooeWdl+inhOiMg+e8luWGmeeahOWJjeerryBVSSDmoYbmnrYg6YG15b6q5Y6f55SfIEhUTUwgQ1NTIEpTIOeahOS5puWGmeW9ouW8jyDmnoHkvY7pl6jmp5sg5ou/5p2l5Y2z55SoIOi0teagoeivvueoi+i1hOaWmeawkemXtOaVtOeQhiDkvIHkuJrnuqfkvY7ku6PnoIHlubPlj7Ag5YmN5ZCO56uv5YiG56a75p625p6E5by65aSn55qE5Luj56CB55Sf5oiQ5Zmo6K6p5YmN5ZCO56uv5Luj56CB5LiA6ZSu55Sf5oiQIOaXoOmcgOWGmeS7u+S9leS7o+eggSDlvJXpoobmlrDnmoTlvIDlj5HmqKHlvI9PbmxpbmVDb2Rpbmcg5Luj56CB55Sf5oiQIOaJi+W3pU1FUkdFIOW4ruWKqUphdmHpobnnm67op6PlhrM3ICXph43lpI3lt6XkvZwg6K6p5byA5Y+R5pu05YWz5rOo5Lia5YqhIOaXouiDveW/q+mAn+aPkOmrmOaViOeOhyDluK7liqnlhazlj7joioLnnIHmiJDmnKwg5ZCM5pe25Y+I5LiN5aSx54G15rS75oCnIOaIkeaYr+S+neaJrCDmnKjmmJPmnagg5YWs5LyX5Y+3IOmrmOe6p+WJjeerr+i/m+mYtiDkvZzogIUg5q+P5aSp5pCe5a6a5LiA6YGT5YmN56uv5aSn5Y6C6Z2i6K+V6aKYIOelneWkp+WutuWkqeWkqei/m+atpSDkuIDlubTlkI7kvJrnnIvliLDkuI3kuIDmoLfnmoToh6rlt7Eg5Ya057695YaZ5Y2a5a6i55qE5Zyw5pa5IOmihOiuoeWGmeWbm+S4quezu+WIlyBKYXZhU2NyaXB05rex5YWl57O75YiXIEphdmFTY3JpcHTkuJPpopjns7vliJcgRVM257O75YiXIFJlYWN057O75YiXIOS4reaWh+WIhuivjSDor43mgKfmoIfms6gg5ZG95ZCN5a6e5L2T6K+G5YirIOS+neWtmOWPpeazleWIhuaekCDor63kuYnkvp3lrZjliIbmnpAg5paw6K+N5Y+R546wIOWFs+mUruivjeefreivreaPkOWPliDoh6rliqjmkZjopoEg5paH5pys5YiG57G76IGa57G7IOaLvOmfs+eugOe5gei9rOaNoiDoh6rnhLbor63oqIDlpITnkIZmbHV0dGVyIOW8gOWPkeiAheW4ruWKqSBBUFAg5YyF5ZCrIGZsdXR0ZXIg5bi455SoIDE0IOe7hOS7tueahGRlbW8g5ryU56S65LiO5Lit5paH5paH5qGjIOS4i+aLieWIt+aWsCDkuIrmi4nliqDovb0g5LqM57qn5Yi35pawIOa3mOWuneS6jOalvOaZuuiDveS4i+aLieWIt+aWsOahhuaetiDmlK/mjIHotornlYzlm57lvLkg6LaK55WM5ouW5YqoIOWFt+acieaegeW8uueahOaJqeWxleaApyDpm4bmiJDkuoblh6DljYHnp43ngqvphbfnmoRIZWFkZXLlkowgRm9vdGVyIOivpemhueebruW3suaIkOWKn+mbhuaIkCBhY3R1YXRvciDnm5HmjqcgYWRtaW4g5Y+v6KeG5YyW55uR5o6nIGxvZ2JhY2sg5pel5b+XIGFvcExvZyDpgJrov4dBT1DorrDlvZV3ZWLor7fmsYLml6Xlv5cg57uf5LiA5byC5bi45aSE55CGIGpzb27nuqfliKvlkozpobXpnaLnuqfliKsgZnJlZW1hcmtlciDmqKHmnb/lvJXmk44gdGh5bWVsZWFmIOaooeadv+W8leaTjiBCZWV0bCDmqKHmnb/lvJXmk44gRW5qb3kg5qih5p2/5byV5pOOIEpkYmNUZW1wbGF0ZSDpgJrnlKhKREJD5pON5L2c5pWw5o2u5bqTIEpQQSDlvLrlpKfnmoRPUk3moYbmnrYgbXliYXRpcyDlvLrlpKfnmoRPUk3moYbmnrYg6YCa55SoTWFwcGVyIOW/q+mAn+aTjeS9nE15YmF0aXMgUGFnZUhlbHBlciDpgJrnlKjnmoRNeWJhdGlz5YiG6aG15o+S5Lu2IG15YmF0aXMgcGx1cyDlv6vpgJ/mk43kvZxNeWJhdGlzIEJlZXRsU1FMIOW8uuWkp+eahE9STeahhuaetiB1IFB5dGhvbui1hOa6kOWkp+WFqOS4reaWh+eJiCDljIXmi6wgV2Vi5qGG5p62IOe9kee7nOeIrOiZqyDmqKHmnb/lvJXmk44g5pWw5o2u5bqTIOaVsOaNruWPr+inhuWMliDlm77niYflpITnkIbnrYkg55SxIOW8gOa6kOWJjeWTqCDlkowgUHl0aG9u5byA5Y+R6ICFIOW+ruS/oeWFrOWPt+WboumYn+e7tOaKpOabtOaWsCDlkLTmganovr7ogIHluIjnmoTmnLrlmajlrabkuaDor77nqIvkuKrkurrnrJTorrBUbyBCZSBUb3AgSmF2YWVyIEphdmHlt6XnqIvluIjmiJDnpZ7kuYvot6/lvqrluo/muJDov5sg5a2m5Lmg5Y2a5a6iU3ByaW5n57O75YiX5rqQ56CBIG1yYmlyZCBjY+iwouiwouWPr+iDveaYr+iuqeS9oOWPl+ebiuWMqua1heeahOiLseivrei/m+mYtuaMh+WNl+mVnOWDj+e9keaYk+S6kemfs+S5kCBOb2RlIGpzIEFQSSBzZXJ2aWNl5b+r6YCfIOeugOWNlemBv+WFjU9PTeeahGphdmHlpITnkIZFeGNlbOW3peWFt+WfuuS6jiBWdWUganMg55qE5bCP56iL5bqP5byA5Y+R5qGG5p62IOS7juW6leWxguaUr+aMgSBWdWUganMg6K+t5rOV5ZKM5p6E5bu65bel5YW35L2T57O7IOS4reaWh+eJiCBBcHBsZSDlrpjmlrkgU3dpZnQg5pWZ56iL5pys6aG555uu5pu+5Yay5Yiw5YWo55CD56ys5LiAIOW5sui0p+mbhumUpuingeacrOmhtemdouacgOW6lemDqCDlj6blrozmlbTnsr7oh7TnmoTnurjotKjniYgg57yW56iL5LmL5rOVIOmdouivleWSjOeul+azleW/g+W+lyDlt7LlnKjkuqzkuJwg5b2T5b2T5LiK6ZSA5ZSu5aW96IC2IOaYr+Wls+ijhVNlY3VyaXR5IEd1aWRlIGZvciBEZXZlbG9wZXJzIOWunueUqOaAp+W8gOWPkeS6uuWRmOWuieWFqOmhu+efpSDpmL/ph4zlt7Tlt7QgTXlTUUwgYmlubG9nIOWinumHj+iuoumYhSDmtojotLnnu4Tku7YgIEVDTUFTY3JpcHQgNuWFpemXqCDmmK/kuIDmnKzlvIDmupDnmoQgSmF2YVNjcmlwdCDor63oqIDmlZnnqIsg5YWo6Z2i5LuL57uNIEVDTUFTY3JpcHQgNiDmlrDlop7nmoTor63ms5XnibnmgKcgIEMgQyDmioDmnK/pnaLor5Xln7rnoYDnn6Xor4bmgLvnu5Mg5YyF5ous6K+t6KiAIOeoi+W6j+W6kyDmlbDmja7nu5PmnoQg566X5rOVIOezu+e7nyDnvZHnu5wg6ZO+5o6l6KOF6L295bqT562J55+l6K+G5Y+K6Z2i6K+V57uP6aqMIOaLm+iBmCDlhoXmjqjnrYnkv6Hmga8g5LiA5qy+5LyY56eA55qE5byA5rqQ5Y2a5a6i5Y+R5biD5bqU55SoICBTb2x1dGlvbnMgdG8gTGVldENvZGUgYnkgR28gMSAlIHRlc3QgY292ZXJhZ2UgcnVudGltZSBiZWF0cyAxICUgTGVldENvZGUg6aKY6Kej5YiG5biD5byP5Lu75Yqh6LCD5bqm5bmz5Y+wWFhMIEpPQiAg6LC357KSIENocm9tZeaPkuS7tuiLsembhOamnCDkuLrkvJjnp4DnmoRDaHJvbWXmj5Lku7blhpnkuIDmnKzkuK3mlofor7TmmI7kuaYg6K6pQ2hyb21l5o+S5Lu26Iux6ZuE5Lus6YCg56aP5Lq657G75YWs5LyX5Y+3IOWKoDEg5ZCM5q2l5pu05paw5LqS6IGU572R5YWs5Y+45oqA5pyv5p625p6EIOW+ruS/oSDmt5jlrp0g5b6u5Y2aIOiFvuiuryDpmL/ph4wg576O5Zui54K56K+EIOeZvuW6piBHb29nbGUgRmFjZWJvb2sgQW1hem9uIGVCYXnnmoTmnrbmnoQg5qyi6L+OUFLooaXlhYVJbnRlbGxpSiBJREVBIOeugOS9k+S4reaWh+S4k+mimOaVmeeoi+eoi+W6j+WRmOaKgOiDveWbvuiwseWJjeerr+mdouivleavj+aXpSAzIDEg5Lul6Z2i6K+V6aKY5p2l6amx5Yqo5a2m5LmgIOaPkOWAoeavj+aXpeWtpuS5oOS4juaAneiAgyDmr4/lpKnov5vmraXkuIDngrkg5q+P5aSp5pep5LiKNeeCuee6r+aJi+W3peWPkeW4g+mdouivlemimCDmrbvno5Xoh6rlt7Eg5oSJ5oKm5aSn5a62IDQg6YGT5YmN56uv6Z2i6K+V6aKY5YWo6Z2i6KaG55uW5bCP56iL5bqPIOi9r+aKgOiDvSDljY7kuLrpuL/okpnmk43kvZzns7vnu58g5LqS6IGU572R6aaW5Lu956iL5bqP5ZGY6ICD5YWs5oyH5Y2XIOeUsTPkvY3lt7Lnu4/ov5vlhaXkvZPliLblhoXnmoTliY3lpKfljoLnqIvluo/lkZjogZTlkIjnjK7kuIogTWFj5b6u5L+h5Yqf6IO95ouT5bGVIOW+ruS/oeaPkuS7tiDlvq7kv6HlsI/liqnmiYsgQSBwbHVnaW4gZm9yIE1hYyBXZUNoYXQgIOacuuWZqOWtpuS5oCDopb/nk5zkuaYg5YWs5byP5o6o5a+86Kej5p6QIOWcqOe6v+mYheivu+WcsOWdgOS4gOasvui9u+mHj+e6pyDpq5jmgKfog70g5Yqf6IO95by65aSn55qE5YaF572R56m/6YCP5Luj55CG5pyN5Yqh5ZmoIOaUr+aMgXRjcCB1ZHAgc29ja3M1IGh0dHDnrYnlh6DkuY7miYDmnInmtYHph4/ovazlj5Eg5Y+v55So5p2l6K6/6Zeu5YaF572R572R56uZIOacrOWcsOaUr+S7mOaOpeWPo+iwg+ivlSBzc2jorr/pl64g6L+c56iL5qGM6Z2iIOWGhee9kWRuc+ino+aekCDlhoXnvZFzb2NrczXku6PnkIbnrYnnrYkg5bm25bim5pyJ5Yqf6IO95by65aSn55qEd2Vi566h55CG56uv5LiA5qy+6Z2i5ZCR5rOb5YmN56uv5Lqn5ZOB56CU5Y+R5YWo55Sf5ZG95ZGo5pyf55qE5pWI546H5bmz5Y+wIOaWh+iogOaWh+e3qOeoi+iqnuiogOa4heWNjuWkp+Wtpuiuoeeul+acuuezu+ivvueoi+aUu+eVpemdouWQkeS6keWOn+eUn+W+ruacjeWKoeeahOmrmOWPr+eUqOa1geaOp+mYsuaKpOe7hOS7tiAgT24gSmF2YSA4IOS4reaWh+eJiCDmnKzmlofljp/mlofnlLHnn6XlkI0gSGFja2VyIEVyaWMgUyBSYXltb25kIOaJgOaSsOWvqyDmlZnkvaDlpoLkvZXmraPnorrnmoTmj5Dlh7rmioDooZPllY/poYzkuKbnjbLlvpfkvaDmu7/mhI/nmoTnrZTmoYggUmVhY3QgTmF0aXZl5oyH5Y2X5rGH6ZuG5LqG5ZCE57G7cmVhY3QgbmF0aXZl5a2m5Lmg6LWE5rqQIOW8gOa6kEFwcOWSjOe7hOS7tjEgRGF5cyBPZiBNTCBDb2Rl5Lit5paH54mI5Y2D5Y+k5YmN56uv5Zu+5paH5pWZ56iLIOi2heivpue7hueahOWJjeerr+WFpemXqOWIsOi/m+mYtuWtpuS5oOeslOiusCDku47pm7blvIDlp4vlrabliY3nq68g5YGa5LiA5ZCN57K+6Ie05LyY6ZuF55qE5YmN56uv5bel56iL5biIIOWFrOS8l+WPtyDljYPlj6Tlo7nlj7cg5L2c6ICFIOWfuuS6jiBSZWFjdCDnmoTmuJDov5vlvI/noJTlj5HmoYbmnrYgaWNlIHdvcmvop4bpopHmkq3mlL7lmajmlK/mjIHlvLnluZUg5aSW5oyC5a2X5bmVIOaUr+aMgea7pOmVnCDmsLTljbAgZ2lm5oiq5Zu+IOeJh+WktOW5v+WRiiDkuK3pl7Tlub/lkYog5aSa5Liq5ZCM5pe25pKt5pS+IOaUr+aMgeWfuuacrOeahOaLluWKqCDlo7Dpn7Mg5Lqu5bqm6LCD6IqCIOaUr+aMgei+ueaSrei+uee8k+WtmCDmlK/mjIHop4bpopHoh6rluKZyb3RhdGlvbueahOaXi+i9rCA5IDI3IOS5i+exuyDph43lipvml4vovazkuI7miYvliqjml4vovaznmoTlkIzmraXmlK/mjIEg5pSv5oyB5YiX6KGo5pKt5pS+IOWIl+ihqOWFqOWxj+WKqOeUuyDop4bpopHliqDovb3pgJ/luqYg5YiX6KGo5bCP56qX5Y+j5pSv5oyB5ouW5YqoIOWKqOeUu+aViOaenCDosIPmlbTmr5Tkvosg5aSa5YiG6L6o546H5YiH5o2iIOaUr+aMgeWIh+aNouaSreaUvuWZqCDov5vluqbmnaHlsI/nqpflj6PpooTop4gg5YiX6KGo5YiH5o2i6K+m5oOF6aG16Z2i5peg57yd5pKt5pS+IHJ0c3AgY29uY2F0IG1wZWcgSnVtcFNlcnZlciDmmK/lhajnkIPpppbmrL7lvIDmupDnmoTloKHlnpLmnLog5piv56ym5ZCIIDRBIOeahOS4k+S4mui/kOe7tOWuieWFqOWuoeiuoeezu+e7nyBMaW51eOWRveS7pOWkp+WFqOaQnOe0ouW3peWFtyDlhoXlrrnljIXlkKtMaW51eOWRveS7pOaJi+WGjCDor6bop6Mg5a2m5LmgIOaQnOmbhiBnaXQgaW8gbGludXggYm9vayBOb2RlIGpzIOWMheaVmeS4jeWMheS8miBieSBhbHNvdGFuZ+WPiOS4gOS4quWwj+WVhuWfjiBsaXRlbWFsbCBTcHJpbmcgQm9vdOWQjuerryBWdWXnrqHnkIblkZjliY3nq68g5b6u5L+h5bCP56iL5bqP55So5oi35YmN56uvIFZ1ZeeUqOaIt+enu+WKqOerr+W+ruS/oSDot7PkuIDot7MgUHl0aG9uIOi+heWKqUphdmHotYTmupDlpKflhajkuK3mlofniYgg5YyF5ous5byA5Y+R5bqTIOW8gOWPkeW3peWFtyDnvZHnq5kg5Y2a5a6iIOW+ruS/oSDlvq7ljZrnrYkg55Sx5Lyv5LmQ5Zyo57q/5oyB57ut5pu05pawICBweXRob27mqKHmi5/nmbvpmYbkuIDkupvlpKflnovnvZHnq5kg6L+Y5pyJ5LiA5Lqb566A5Y2V55qE54is6JmrIOW4jOacm+WvueS9oOS7rOacieaJgOW4ruWKqSDvuI8g5aaC5p6c5Zac5qyi6K6w5b6X57uZ5Liqc3RhcuWTpiBDIOmCo+S6m+S6iyDnvZHnu5zniKzomavlrp7miJgg5reY5a6dIOS6rOS4nCDnvZHmmJPkupEgQuermSAxMjMgNiDmipbpn7Mg56yU6Laj6ZiBIOa8q+eUu+Wwj+ivtOS4i+i9vSDpn7PkuZDnlLXlvbHkuIvovb3nrYlkZWVwbGVhcm5pbmcgYWkg5ZC05oGp6L6+6ICB5biI55qE5rex5bqm5a2m5Lmg6K++56iL56yU6K6w5Y+K6LWE5rqQIFNwcmluZyBCb2905Z+656GA5pWZ56iLIFNwcmluZyBCb290IDIgeOeJiOacrOi/nui9veS4rSDluK7liqkgQW5kcm9pZCBBcHAg6L+b6KGM57uE5Lu25YyW5pS56YCg55qE6Lev55Sx5qGG5p62IOacgOaOpei/keWOn+eUn0FQUOS9k+mqjOeahOmrmOaAp+iDveahhuaetuWfuuS6jlZ1ZTMgRWxlbWVudCBQbHVzIOeahOWQjuWPsOeuoeeQhuezu+e7n+ino+WGs+aWueahiOeoi+W6j+WRmOWmguS9leS8mOmbheeahOaMo+mbtuiKsemSsSAyIOeJiCDljYfnuqfkuLrlsI/kuabkuoYg5LuOSmF2YeWfuuehgCBKYXZhV2Vi5Z+656GA5Yiw5bi455So55qE5qGG5p625YaN5Yiw6Z2i6K+V6aKY6YO95pyJ5a6M5pW055qE5pWZ56iLIOWHoOS5jua2teebluS6hkphdmHlkI7nq6/lv4XlpIfnmoTnn6Xor4bngrlzcHJpbmcgYm9vdCDlrp7ot7XlrabkuaDmoYjkvosg5pivIHNwcmluZyBib290IOWIneWtpuiAheWPiuaguOW/g+aKgOacr+W3qeWbuueahOacgOS9s+Wunui3tSDlj6blpJblhpnljZrlrqIg55SoIE9wZW5Xcml0ZSDmnIDlpb3nlKjnmoQgVjJSYXkg5LiA6ZSu5a6J6KOF6ISa5pysIOeuoeeQhuiEmuacrOS4reWbveeoi+W6j+WRmOWuueaYk+WPkemfs+mUmeivr+eahOWNleivjSDnu5/orqHlrabkuaDmlrnms5Ug55qE5Luj56CB5a6e546w5YWz5LqOUHl0aG9u55qE6Z2i6K+V6aKY5pys6aG555uu5bCGIOWKqOaJi+Wtpua3seW6puWtpuS5oCBEaXZlIGludG8gRGVlcCBMZWFybmluZyDljp/kuabkuK3nmoRNWE5ldOWunueOsOaUueS4ulB5VG9yY2jlrp7njrAg5o+Q6auYIEFuZHJvaWQgVUkg5byA5Y+R5pWI546H55qEIFVJIOW6k+WJjeerr+eyvuivu+WRqOWIiiDluK7kvaDnkIbop6PmnIDliY3msr8g5a6e55So55qE5oqA5pyvICDnmoTlpYfmioDmt6vlt6fml7bpl7TpgInmi6nlmagg55yB5biC5Yy65LiJ57qn6IGU5YqoIFB5dGhvbueIrOiZq+S7o+eQhklQ5rGgIHByb3h5IHBvb2wgTGVldENvZGUg5Yi36aKY5pS755WlIDIg6YGT57uP5YW46aKY55uu5Yi36aKY6aG65bqPIOWFsTYgd+Wtl+eahOivpue7huWbvuinoyDop4bpopHpmr7ngrnliZbmnpAgNSDkvZnlvKDmgJ3nu7Tlr7zlm74g5LuO5q2k566X5rOV5a2m5Lmg5LiN5YaN6L+36IyrIOadpeeci+eciyDkvaDkvJrlj5HnjrDnm7jop4HmgajmmZog5LiA5Liq5Z+65LqOIGVsZWN0cm9uIOeahOmfs+S5kOi9r+S7tkZsdXR0ZXIg6LaF5a6M5pW055qE5byA5rqQ6aG555uuIOWKn+iDveS4sOWvjCDpgILlkIjlrabkuaDlkozml6XluLjkvb/nlKggR1NZR2l0aHViQXBw57O75YiX55qE5LyY5Yq/IOaIkeS7rOebruWJjeW3sue7j+aLpeacieWbm+S4queJiOacrCDlip/og73pvZDlhagg6aG555uu5qGG5p625YaF5oqA5pyv5raJ5Y+K6Z2i5bm/IOWujOaIkOW6pumrmCDmjIHnu63nu7TmiqQg6YWN5aWX5paH56ugIOmAguWQiOWFqOmdouWtpuS5oCDlr7nmr5Tlj4LogIMg6Leo5bmz5Y+w55qE5byA5rqQR2l0aHVi5a6i5oi356uvQXBwIOabtOWlveeahOS9k+mqjCDmm7TkuLDlr4znmoTlip/og70g5peo5Zyo5pu05aW955qE5pel5bi4566h55CG5ZKM57u05oqk5Liq5Lq6R2l0aHViIOaPkOS+m+abtOWlveabtOaWueS+v+eahOmpvui9puS9k+mqjM6jIOWQjOasvldlZXjniYjmnKzlkIzmrL5SZWFjdCBOYXRpdmXniYjmnKwgaHR0cHMgZyDov5nmmK/kuIDkuKrnlKjkuo7mmL7npLrlvZPliY3nvZHpgJ8gQ1BV5Y+K5YaF5a2Y5Yip55So546H55qE5qGM6Z2i5oKs5rWu56qX6L2v5Lu2IOW5tuaUr+aMgeS7u+WKoeagj+aYvuekuiDmlK/mjIHmm7TmjaLnmq7ogqQg5piv5LiA5Liq6Leo5bmz5Y+w55qE5by65Yqg5a+G5peg54m55b6B55qE5Luj55CG6L2v5Lu2IOmbtumFjee9riBWMnJheVUg5Z+65LqOdjJyYXnmoLjlv4PnmoRtYWPniYjlrqLmiLfnq68g55So5LqO56eR5a2m5LiK572RIOS9v+eUqHN3aWZ057yW5YaZIOaUr+aMgXZtZXNzIHNoYWRvd3NvY2tzIHNvY2tzNeetieacjeWKoeWNj+iuriDmlK/mjIHorqLpmIUg5pSv5oyB5LqM57u056CBIOWJqui0tOadv+WvvOWFpSDmiYvliqjphY3nva4g5LqM57u056CB5YiG5Lqr562J566X5rOV5qih5p2/IOacgOenkeWtpueahOWIt+mimOaWueW8jyDmnIDlv6vpgJ/nmoTliLfpopjot6/lvoQg5L2g5YC85b6X5oul5pyJIOe7j+WFuOe8lueoi+S5puexjeWkp+WFqCDmtrXnm5Yg6K6h566X5py657O757uf5LiO572R57ucIOezu+e7n+aetuaehCDnrpfms5XkuI7mlbDmja7nu5PmnoQg5YmN56uv5byA5Y+RIOWQjuerr+W8gOWPkSDnp7vliqjlvIDlj5Eg5pWw5o2u5bqTIOa1i+ivlSDpobnnm67kuI7lm6LpmJ8g56iL5bqP5ZGY6IGM5Lia5L+u54K8IOaxguiBjOmdouivleetiXdhbmdFZGl0b3Ig6L276YeP57qnd2Vi5a+M5paH5pys5qGG5YmN56uv6Leo5qGG5p626Leo5bmz5Y+w5qGG5p62IOavj+S4qiBKYXZhU2NyaXB0IOW3peeoi+W4iOmDveW6lOaHgueahDMz5Liq5qaC5b+1IGxlb25hcmRvbXNv5LiA5Liq5Y+v5Lul6KeC55yL5Zu95YaF5Li75rWB6KeG6aKR5bmz5Y+w5omA5pyJ6KeG6aKR55qE5a6i5oi356uvQW5kcm9pZOW8gOWPkeS6uuWRmOS4jeW+l+S4jeaUtumbhueahOW3peWFt+exu+mbhuWQiCDmlK/ku5jlrp3mlK/ku5gg5b6u5L+h5pSv5LuYIOe7n+S4gOS4i+WNlSDlvq7kv6HliIbkuqsgWmlwNGrljovnvKkg5pSv5oyB5YiG5Y235Y6L57yp5LiO5Yqg5a+GIOS4gOmUrumbhuaIkFVDcm9w6YCJ5oup5ZyG5b2i5aS05YOPIOS4gOmUrumbhuaIkOS6jOe7tOeggeWSjOadoeW9oueggeeahOaJq+aPj+S4jueUn+aIkCDluLjnlKhEaWFsb2cgV2ViVmlld+eahOWwgeijheWPr+aSreaUvuinhumikSDku7/mlpfpsbzmu5Hliqjpqozor4HnoIEgVG9hc3TlsIHoo4Ug6ZyH5YqoIEdQUyBMb2NhdGlvbuWumuS9jSDlm77niYfnvKnmlL4gRXhpZiDlm77niYfmt7vliqDlnLDnkIbkvY3nva7kv6Hmga8g57uP57qs5bqmIOibm+e9keetiee6pyDpopzoibLpgInmi6nlmaggQXJjR2lzIFZUUEsg57yW6K+R6L+Q6KGM5LiA5LiL6K+05LiN5a6a5Lya5om+5Yiw5oOK5ZacIDEyMyA2IOi0reelqOWKqeaJiyDmlK/mjIHpm4bnvqQg5aSa6LSm5Y+3IOWkmuS7u+WKoei0reelqOS7peWPiiBXZWIg6aG16Z2i566h55CGICDnvJbnqIvpmo/mg7Mg5pS26JeP55qE55S15a2Q5Lmm5riF5Y2VIOWkmuS4quWtpuenkSDlkKvkuIvovb3pk77mjqUgIEJhbm5lciAyIOadpeS6hiBBbmRyb2lk5bm/5ZGK5Zu+54mH6L2u5pKt5o6n5Lu2IOWGhemDqOWfuuS6jlZpZXdQYWdlcjLlrp7njrAgSW5kaWNhdG9y5ZKMVUnpg73lj6/ku6Xoh6rlrprkuYkgIOmbtuS7o+eggSDng63mm7TmlrAg6Ieq5Yqo5YyWIE9STSDlupMg5ZCO56uv5o6l5Y+j5ZKM5paH5qGj6Zu25Luj56CBIOWJjeerryDlrqLmiLfnq68g5a6a5Yi26L+U5ZueIEpTT04g55qE5pWw5o2u5ZKM57uT5p6EIExpbnV4IFdpbmRvd3MgbWFjT1Mg6Leo5bmz5Y+wIFYyUmF5IOWuouaIt+erryDmlK/mjIHkvb/nlKggQyBRdCDlvIDlj5Eg5Y+v5ouT5bGV5o+S5Lu25byP6K6+6K6hIHdhbGxlIOeTpuWKmyBEZXZvcHPlvIDmupDpobnnm67ku6PnoIHpg6jnvbLlubPlj7Dln7rkuo4gbm9kZSBqcyBNb25nb2RiIOaehOW7uueahOWQjuWPsOezu+e7nyBqcyDmupDnoIHop6PmnpDkuIDkuKrmtrXnm5blha3kuKrkuJPmoI/liIbluIPlvI/mtojmga/pmJ/liJcg5YiG5biD5byP5LqL5Yqh55qE5LuT5bqTIOW4jOacm+iDluWPi+Wwj+aJi+S4gOaKliDlj7PkuIrop5LmnaXkuKogU3RhciDmhJ/mgakgMSAyNOWfuuS6jiB2dWUgZWxlbWVudCB1aSDnmoTlkI7lj7DnrqHnkIbns7vnu5/no4Hlipvpk77mjqXogZrlkIjmkJzntKLkuK3ljY7kurrmsJHlhbHlkozlm73ooYzmlL/ljLrliJIg55yB57qnIOecgeS7veebtOi+luW4guiHquayu+WMuiDlnLDnuqcg5Z+O5biCIOWOv+e6pyDljLrljr8g5Lmh57qnIOS5oemVh+ihl+mBkyDmnZHnuqcg5p2R5aeU5Lya5bGF5aeU5LyaIOS4reWbveecgeW4guWMuumVh+adkeS6jOe6p+S4iee6p+Wbm+e6p+S6lOe6p+iBlOWKqOWcsOWdgOaVsOaNriBpT1PlvIDlj5HluLjnlKjkuInmlrnlupMg5o+S5Lu2IOefpeWQjeWNmuWuouetieetiUxlZXRDb2Rl6aKY6KejIDE1MemBk+mimOWujOaVtOeJiO+8j+S4reaWh+aWh+ahiOaOkueJiOaMh+WMl+acgOiJr+W/g+eahCBQeXRob24g5pWZ56iLIOS4muWGheS4uuaVsOS4jeWkmuiHtOWKm+S6juaegeiHtOS9k+mqjOeahOi2heW8uuWFqOiHqueglOi3qOW5s+WPsCB3aW5kb3dzIGFuZHJvaWQgaU9TIOa1geWqkuS9k+WGheaguCDpgJrov4fmqKHlnZfljJboh6rnlLHnu4TlkIgg5pSv5oyB5a6e5pe2UlRNUOaOqOa1gSBSVFNQ5o6o5rWBIFJUTVDmkq3mlL7lmaggUlRTUOaSreaUvuWZqCDlvZXlg48g5aSa6Lev5rWB5aqS5L2T6L2s5Y+RIOmfs+inhumikeWvvOaSrSDliqjmgIHop4bpopHlkIjmiJAg6Z+z6aKR5re36Z+zIOebtOaSreS6kuWKqCDlhoXnva7ovbvph4/nuqdSVFNQ5pyN5Yqh562JIOavlOW/q+abtOW/qyDkuJrnlYznnJ/mraPpnaDosLHnmoTotoXkvY7lu7bov5/nm7Tmkq1TREsgMeenkuWGhSDkvY7lu7bov5/mqKHlvI/kuIsyIDQgbXMgIOS4gOS4qiBQSFAg5b6u5L+hIFNESyDvuI8g6Leo5bmz5Y+w5qGM6Z2i56uv6KeG6aKR6LWE5rqQ5pKt5pS+5ZmoIOeugOa0geaXoOW5v+WRiiDlhY3otLnpq5jpopzlgLwg5ZCO5Y+w566h55CG5Li757q/54mI5pys5Z+65LqO5LiJ6ICF5bm26KGM5byA5Y+R57u05oqkIOWQjOaXtuaUr+aMgeeUteiEkSDmiYvmnLog5bmz5p2/IOWIh+aNouWIhuaUr+afpeeci+S4jeWQjOeahHZ1ZeeJiOacrCBlbGVtZW50IHBsdXPniYjmnKzlt7Llj5HluIMgdnVlMyB2dWUzIHZ1ZSB2dWUzIHggdnVlIGpzIOeoi+W6j+aXoOWbveeVjCDkvYbnqIvluo/lkZjmnInlm73nlYwg5Lit5Zu95Zu95a625bCK5Lil5LiN5a655oyR6KGFIOWmguaenOaCqOWcqOeJueauiuaXtuacnyDmraTpobnnm67mmK/mnLrlmajlrabkuaAgTWFjaGluZSBMZWFybmluZyDmt7HluqblrabkuaAgRGVlcCBMZWFybmluZyBOTFDpnaLor5XkuK3luLjogIPliLDnmoTnn6Xor4bngrnlkozku6PnoIHlrp7njrAg5Lmf5piv5L2c5Li65LiA5Liq566X5rOV5bel56iL5biI5b+F5Lya55qE55CG6K665Z+656GA55+l6K+GIOWknOivuyDpgJrov4cgYmlsaWJpbGkg5Zyo57q/55u05pKt55qE5pa55byP5YiG5LqrIEdvIOebuOWFs+eahOaKgOacr+ivnemimCDmr4/lpKnlpKflrrblnKjlvq7kv6EgdGVsZWdyYW0gU2xhY2sg5LiK5Y+K5pe25rKf6YCa5Lqk5rWB57yW56iL5oqA5pyv6K+d6aKYIEdpdEh1YkRhaWx5IOWIhuS6q+WGheWuueWumuacn+aVtOeQhuS4juWIhuexuyDmrKLov47mjqjojZAg6Ieq6I2Q6aG555uuIOiuqeabtOWkmuS6uuefpemBk+S9oOeahOmhueebriAg5pSv5oyB5aSa5a625LqR5a2Y5YKo55qE5LqR55uY57O757uf5py65Zmo5a2m5Lmg55u45YWz5pWZ56iLRGF0YVjmmK/pmL/ph4zkupFEYXRhV29ya3PmlbDmja7pm4bmiJDnmoTlvIDmupDniYjmnKwgIOi/memHjOaYr+WGmeWNmuWuoueahOWcsOaWuSBIYWxmcm9zdCBGaWVsZCDlhrDpnJzkuYvlnLBtYWxs5a2m5Lmg5pWZ56iLIOaetuaehCDkuJrliqEg5oqA5pyv6KaB54K55YWo5pa55L2N6Kej5p6QIG1hbGzpobnnm64gNCBrIHN0YXIg5piv5LiA5aWX55S15ZWG57O757ufIOS9v+eUqOeOsOmYtuauteS4u+a1geaKgOacr+WunueOsCDmtrXnm5bkuobnrYnmioDmnK8g6YeH55SoRG9ja2Vy5a655Zmo5YyW6YOo572yICBjaGljayDmmK/kvb/nlKggTm9kZSBqcyDlkowgTW9uZ29EQiDlvIDlj5HnmoTnpL7ljLrns7vnu5/kuIDkuKrpnZ7luLjpgILlkIhJVOWboumYn+eahOWcqOe6v0FQSeaWh+ahoyDmioDmnK/mlofmoaPlt6XlhbfmsYfmgLvlkITlpKfkupLogZTnvZHlhazlj7jlrrnmmJPogIPlr5/nmoTpq5jpopFsZWV0Y29kZemimCAxIENoaW5lc2UgV29yZCBWZWN0b3JzIOS4iueZvuenjemihOiuree7g+S4reaWh+ivjeWQkemHjyBBbmRyb2lk5byA5rqQ5by55bmV5byV5pOOIOeDiOeEsOW8ueW5leS9vyDvvZ7mt7HluqblrabkuaDmoYbmnrZQeVRvcmNoIOWFpemXqOS4juWunuaImCDnvZHmmJPkupHpn7PkuZDlkb3ku6TooYzniYjmnKwg5a+55byA5Y+R5Lq65ZGY5pyJ55So55qE5a6a5b6LIOeQhuiuuiDljp/liJnlkozmqKHlvI9UZWFjaFlvdXJzZWxmQ1Mg55qE5Lit5paH57+76K+R6auY6aKc5YC855qE56ys5LiJ5pa5572R5piT5LqR5pKt5pS+5ZmoIOaUr+aMgSBXaW5kb3dzIG1hY09TIExpbnV4IHNwcmluZyBjbG91ZCB2dWUgb0F1dGgyIOWFqOWutuahtuWunuaImCDliY3lkI7nq6/liIbnprvmqKHmi5/llYbln44g5a6M5pW055qE6LSt54mp5rWB56iLIOWQjuerr+i/kOiQpeW5s+WPsCDlj6/ku6Xlrp7njrDlv6vpgJ/mkK3lu7rkvIHkuJrnuqflvq7mnI3liqHpobnnm64g5pSv5oyB5b6u5L+h55m75b2V562J5LiJ5pa555m75b2VICBDaGluZXNlIHN0aWNrZXIgcGFjayBNb3JlIGpveSDooajmg4XljIXnmoTljZrnianppoYgR2l0aHVi5pyA5pyJ5q+S55qE5LuT5bqTIOS4reWbveihqOaDheWMheWkp+mbhuWQiCDogZrmrKLkuZAgTGFudGVybuWumOaWueeJiOacrOS4i+i9vSDok53nga8g57+75aKZIOS7o+eQhiDnp5HlrabkuIrnvZEg5aSW572RIOWKoOmAn+WZqCDmoq/lrZAg6Lev55Sx5LiA5qy+5YWl6Zeo57qn55qE5Lq66IS4IOinhumikSDmloflrZfmo4DmtYvku6Xlj4ror4bliKvnmoTpobnnm64gdnVlMiB2dWUgcm91dGVyIHZ1ZXgg5YWl6Zeo6aG555uuUGFuRG93bmxvYWTnmoTkuKrkurrnu7TmiqTniYjmnKwg5LiA5Liq5Z+65LqOU3ByaW5nIEJvb3QgTXlCYXRpc+eahOenjeWtkOmhueebriDnlKjkuo7lv6vpgJ/mnoTlu7rkuK3lsI/lnotBUEkgUkVTVGZ1bCBBUEnpobnnm64gaU9TIGludGVydmlldyBxdWVzdGlvbnMgaU9T6Z2i6K+V6aKY6ZuG6ZSmIOmZhOetlOahiCDlrabkuaBxcee+pOaIliBUZWxlZ3JhbSDnvqTkuqTmtYHkuLrkupLogZTnvZFJVOS6uuaJk+mAoOeahOS4reaWh+eJiGF3ZXNvbWUgZ2/lvLrlpKcg5Y+v5a6a5Yi2IOaYk+aJqeWxleeahCBWaWV3UGFnZXIg5oyH56S65Zmo5qGG5p62IOaYr+eahOacgOS9s+abv+S7o+WTgSDmlK/mjIHop5LmoIcg5pu05pSv5oyB5Zyo6Z2eVmlld1BhZ2Vy5Zy65pmv5LiL5L2/55SoIOS9v+eUqGhpZGUgc2hvdyDliIfmjaJGcmFnbWVudOaIluS9v+eUqHNlIEt1YmVybmV0ZXPkuK3mlofmjIfljZcg5LqR5Y6f55Sf5bqU55So5p625p6E5a6e6Le15omL5YaMRm9yIG1hY09TIOeZvuW6pue9keebmCDnoLTop6NTVklQIOS4i+i9vemAn+W6pumZkOWItiDmnrbmnoTluIjmioDmnK/lm77osLEg5Yqp5L2g5pep5pel5oiQ5Li65p625p6E5biIbWFsbCBhZG1pbiB3ZWLmmK/kuIDkuKrnlLXllYblkI7lj7DnrqHnkIbns7vnu5/nmoTliY3nq6/pobnnm64g5Z+65LqOVnVlIEVsZW1lbnTlrp7njrAg5Li76KaB5YyF5ous5ZWG5ZOB566h55CGIOiuouWNleeuoeeQhiDkvJrlkZjnrqHnkIYg5L+D6ZSA566h55CGIOi/kOiQpeeuoeeQhiDlhoXlrrnnrqHnkIYg57uf6K6h5oql6KGoIOi0ouWKoeeuoeeQhiDmnYPpmZDnrqHnkIYg6K6+572u562J5Yqf6IO9IOe9keaYk+S6kemfs+S5kOesrOS4ieaWuSDnvJbnqIvpmo/mg7Mg5pW055CG55qEIOWkquWtkOWFmuWFs+ezu+e9kee7nCDkuJPpl6jmj63pnLLotbXlm73nmoTmnYPotLXln7rkuo5naW4gdnVl5pCt5bu655qE5ZCO5Y+w566h55CG57O757uf5qGG5p62IOmbhuaIkGp3dOmJtOadgyDmnYPpmZDnrqHnkIYg5Yqo5oCB6Lev55SxIOWIhumhteWwgeijhSDlpJrngrnnmbvlvZXmi6bmiKog6LWE5rqQ5p2D6ZmQIOS4iuS8oOS4i+i9vSDku6PnoIHnlJ/miJDlmagg6KGo5Y2V55Sf5oiQ5ZmoIOmAmueUqOW3peS9nOa1geetieWfuuehgOWKn+iDvSDkupTliIbpkp/kuIDlpZdDVVJE5YmN5ZCO56uv5Luj56CBIOebrlZVRTPniYjmnKzmraPlnKjph43mnoQg5qyi6L+OaXNzdWXlkoxwciAyN+WkqeaIkOS4ukphdmHlpKfnpZ7kuIDkuKrln7rkuo7mtY/op4jlmajnq68gSlMg5a6e546w55qE5Zyo57q/5Luj55CG57yW56iL55S15a2Q5LmmIOeUteWtkOS5piDnvJbnqIvkuabnsY0g5YyF5ous5Lq65bel5pm66IO9IOWkp+aVsOaNruexuyDlubblj5HnvJbnqIsg5pWw5o2u5bqT57G7IOaVsOaNruaMluaOmCDmlrDpnaLor5Xpopgg5p625p6E6K6+6K6hIOeul+azleezu+WIlyDorqHnrpfmnLrnsbsg6K6+6K6h5qih5byPIOi9r+S7tua1i+ivlSDph43mnoTkvJjljJYg562J5pu05aSa5YiG57G7QURCIFVzYWdlIENvbXBsZXRlIEFEQiDnlKjms5XlpKflhajkuoznu7TnoIHnlJ/miJDlmagg5pSv5oyBIGdpZiDliqjmgIHlm77niYfkuoznu7TnoIEgVmltIOS7juWFpemXqOWIsOeyvumAmumYv+W4g+mHj+WMluS6pOaYk+ezu+e7nyDogqHnpagg5pyf5p2DIOacn+i0pyDmr5TnibnluIEg5py65Zmo5a2m5LmgIOWfuuS6jnB5dGhvbueahOW8gOa6kOmHj+WMluS6pOaYkyDph4/ljJbmipXotYTmnrbmnoTkuIDkuKrnroDmtIHkvJjpm4XnmoRoZXhv5Li76aKYIFdpa2kgb2YgT0kgSUNQQyBmb3IgZXZlcnlvbmUg5p+Q5aSn5Z6L5ri45oiP57q/5LiK5pS755WlIOWGheWQq+eCq+mFt+eul+acr+mtlOazlSBHb29nbGUg5byA5rqQ6aG555uu6aOO5qC85oyH5Y2XIOS4reaWh+eJiCAgR2l0IEFXUyBHb29nbGUg6ZWc5YOPIFNTIFNTUiBWTUVTU+iKgueCueihjOS4mueglOeptuaKpeWRiueahOefpeivhuWCqOWkh+W6kyBjaW0gY3Jvc3MgSU0g6YCC55So5LqO5byA5Y+R6ICF55qE5YiG5biD5byP5Y2z5pe26YCa6K6v57O757uf5b6u5L+h5bCP56iL5bqP5byA5rqQ6aG555uu5bqT5rGH5oC75q+P5aSp5pu05pawIOWFqOe9keeDremXqCBCVCBUcmFja2VyIOWIl+ihqCDlpKnnlKhHb+WKqOaJi+WGmSDku47pm7blrp7njrDns7vliJflvLrlpKfnmoTlk5Tlk6nlk5Tlk6nlop7lvLrohJrmnKwg5LiL6L296KeG6aKRIOmfs+S5kCDlsIHpnaIg5by55bmVIOeugOWMluebtOaSremXtCDor4TorrrljLog6aaW6aG1IOiHquWumuS5iemhtuagjyDliKDpmaTlub/lkYog5aSc6Ze05qih5byPIOinpuWxj+iuvuWkh+aUr+aMgUV2aWwgSHVhd2VpIOWNjuS4uuS9nOi/h+eahOaBtkFuZHJvaWTkuIrkuIDkuKrkvJjpm4Ug5LiH6IO96Ieq5a6a5LmJVUkg5Lu/aU9TIOaUr+aMgeWeguebtCDmsLTlubPmlrnlkJHliIfmjaIg5pSv5oyB5ZGo6KeG5Zu+IOiHquWumuS5ieWRqOi1t+WniyDmgKfog73pq5jmlYjnmoTml6Xljobmjqfku7Yg5pSv5oyB54Ot5o+S5ouU5a6e546w55qEVUnlrprliLYg5pSv5oyB5qCH6K6wIOiHquWumuS5ieminOiJsiDlhpzljoYg6Ieq5a6a5LmJ5pyI6KeG5Zu+5ZCE56eN5pi+56S65qih5byP562JIENhbnZhc+e7mOWItiDpgJ/luqblv6sg5Y2g55So5YaF5a2Y5L2OIOS9oOecn+eahOaDs+S4jeWIsOaXpeWOhuWxheeEtui/mOWPr+S7peWmguatpOS8mOmbheW3suS4jeWGjee7tOaKpOenkeWtpuS4iue9keaPkuS7tueahOemu+e6v+WuieijheWMheWCqOWtmOWcqOi/memHjFRoaW5rUEhQIEZyYW1ld29yayDljYHlubTljKDlv4PnmoTpq5jmgKfog71QSFDmoYbmnrYgSmF2YSDnqIvluo/lkZjnnLzkuK3nmoQgTGludXgg5LiA5Liq5pSv5oyB5aSa6YCJIOmAieWOn+WbvuWSjOinhumikeeahOWbvueJh+mAieaLqeWZqCDlkIzml7bmnInpooTop4gg6KOB5Ymq5Yqf6IO9IOaUr+aMgWhzd2ViIGhhyopzIHfJm2Ig5piv5LiA5Liq5Z+65LqOc3ByaW5nIGJvb3QgMiB45byA5Y+RIOmmluS4quS9v+eUqOWFqOWTjeW6lOW8j+e8lueoi+eahOS8geS4mue6p+WQjuWPsOeuoeeQhuezu+e7n+WfuuehgOmhueebriDlrabkuaDlvLrlm70g5oeS5Lq65Yi35YiG5bel5YW3IOiHquWKqOWtpuS5oHd4UGFyc2Ug5b6u5L+h5bCP56iL5bqP5a+M5paH5pys6Kej5p6Q6Ieq5a6a5LmJ57uE5Lu2IOaUr+aMgUhUTUzlj4ptYXJrZG93buino+aekCBuZXdiZWUgbWFsbCDpobnnm64g5paw6JyC5ZWG5Z+OIOaYr+S4gOWll+eUteWVhuezu+e7nyDljIXmi6wgbmV3YmVlIG1hbGwg5ZWG5Z+O57O757uf5Y+KIG5ld2JlZSBtYWxsIGFkbWluIOWVhuWfjuWQjuWPsOeuoeeQhuezu+e7nyDln7rkuo4gU3ByaW5nIEJvb3QgMiBYIOWPiuebuOWFs+aKgOacr+agiOW8gOWPkSDliY3lj7DllYbln47ns7vnu5/ljIXlkKvpppbpobXpl6jmiLcg5ZWG5ZOB5YiG57G7IOaWsOWTgeS4iue6vyDpppbpobXova7mkq0g5ZWG5ZOB5o6o6I2QIOWVhuWTgeaQnOe0oiDllYblk4HlsZXnpLog6LSt54mp6L2mIOiuouWNlee7k+eulyDorqLljZXmtYHnqIsg5Liq5Lq66K6i5Y2V566h55CGIOS8muWRmOS4reW/gyDluK7liqnkuK3lv4PnrYnmqKHlnZcg5ZCO5Y+w566h55CG57O757uf5YyF5ZCr5pWw5o2u6Z2i5p2/IOi9ruaSreWbvueuoeeQhiDllYblk4HnrqHnkIYg6K6i5Y2V566h55CGIOS8muWRmOeuoeeQhiDliIbnsbvnrqHnkIYg6K6+572u562J5qih5Z2XICDmnIDlhajnmoTliY3nq6/otYTmupDmsYfmgLvku5PlupMg5YyF5ous5YmN56uv5a2m5LmgIOW8gOWPkei1hOa6kCDmsYLogYzpnaLor5XnrYkg5Lit5paH57+76K+R5omL5YaZ5a6e546w5p2O6IiqIOe7n+iuoeWtpuS5oOaWueazlSDkuabkuK3lhajpg6jnrpfms5UgUHl0aG9uIOaKlumfs+acuuWZqOS6uiDorrrlpoLkvZXlnKjmipbpn7PkuIrmib7liLDmvILkuq7lsI/lp5Dlp5DvvJ8gIO+4j0Egc3RhdGljIGJsb2cgd3JpdGluZyBjbGllbnQg5LiA5Liq6Z2Z5oCB5Y2a5a6i5YaZ5L2c5a6i5oi356uvIOi2hee6p+mAn+afpeihqCDnvJbnqIvor63oqIAg5qGG5p625ZKM5byA5Y+R5bel5YW355qE6YCf5p+l6KGoIOWNleS4quaWh+S7tuWMheWQq+S4gOWIh+S9oOmcgOimgeefpemBk+eahOS4nOilvyDov4Hnp7vlrabkuaDliY3nq6/kvY7ku6PnoIHmoYbmnrYg6YCa6L+HIEpTT04g6YWN572u5bCx6IO955Sf5oiQ5ZCE56eN6aG16Z2iIOaKgOacr+mdouivleacgOWQjuWPjemXrumdouivleWumOeahOivnU1hY2hpbmUgTGVhcm5pbmcgWWVhcm5pbmcg5Lit5paH54mIIOacuuWZqOWtpuS5oOiuree7g+enmOexjSBBbmRyZXcgTmcg6JGX6LaK5p2l6LaK5aSa55qE572R56uZ5YW35pyJ5Y+N54is6Jmr54m55oCnIOacieeahOeUqOWbvueJh+makOiXj+WFs+mUruaVsOaNriDmnInnmoTkvb/nlKjlj43kurrnsbvnmoTpqozor4HnoIEg5bu656uL5Y+N5Y+N54is6Jmr55qE5Luj56CB5LuT5bqTIOmAmui/h+S4juS4jeWQjOeJueaAp+eahOe9keermeWBmuaWl+S6iSDml6DmgbbmhI8g5o+Q6auY5oqA5pyvIOasoui/juaPkOS6pOmavuS7pemHh+mbhueahOe9keermSDlm6Dlt6XkvZzljp/lm6Ag6aG555uu5pqC5YGcIOacrOmhueebruaUtuiXj+i/meS6m+W5tOadpeeci+i/h+aIluiAheWQrOi/h+eahOS4gOS6m+S4jemUmeeahOW4uOeUqOeahOS4iuWNg+acrOS5puexjSDmsqHlh4bkvaDmg7Pmib7nmoTkuablsLHlnKjov5nph4zlkaIg5YyF5ZCr5LqG5LqS6IGU572R6KGM5Lia5aSn5aSa5pWw5Lmm57GN5ZKM6Z2i6K+V57uP6aqM6aKY55uu562J562JIOacieS6uuW3peaZuuiDveezu+WIlyDluLjnlKjmt7HluqblrabkuaDmoYbmnrZUZW5zb3JGbG93IHB5dG9yY2gga2VyYXMgTkxQIOacuuWZqOWtpuS5oCDmt7HluqblrabkuaDnrYnnrYkg5aSn5pWw5o2u57O75YiXIFNwYXJrIEhhZG9vcCBTY2FsYSBrYWZrYeetiSDnqIvluo/lkZjlv4Xkv67ns7vliJcgQyBDIGphdmEg5pWw5o2u57uT5p6EIGxpbnV4IOiuvuiuoeaooeW8jyDmlbDmja7lupPnrYnnrYkgIOS6uuS6uuW9seinhmJvdCDlrozlhajlr7nmjqXkurrkurrlvbHop4blhajpg6jml6DliKDlh4/otYTmupBTcHJpbmcgQ2xvdWTln7rnoYDmlZnnqIsg5oyB57ut6L+e6L295pu05paw5Lit5LiA5Liq55So5LqO5ZyoIG1hY09TIOS4iuW5s+a7keS9oOeahOm8oOagh+a7muWKqOaViOaenOaIluWNleeLrOiuvue9rua7muWKqOaWueWQkeeahOWwj+W3peWFtyDorqnkvaDnmoTmu5rova7niL3lpoLop6bmjqfmnb/pmL/ph4zlpojlpojliY3nq6/lm6LpmJ/lh7rlk4HnmoTlvIDmupDmjqXlj6PnrqHnkIblt6XlhbdSQVDnrKzkuozku6PotoXovbvph4/nuqfkuK3mlodvY3Ig5pSv5oyB56uW5o6S5paH5a2X6K+G5YirIOaUr+aMgW5jbm4gbW5uIHRubuaOqOeQhuaAu+aooeWei+S7hTQgN00g5b6u5L+h5YWo5bmz5Y+wIFNESyBTZW5wYXJjIFdlaXhpbiBmb3IgQyDmlK/mjIEgTkVUIEZyYW1ld29yayDlj4ogTkVUIENvcmUgTkVUIDYg5bey5pSv5oyB5b6u5L+h5YWs5LyX5Y+3IOWwj+eoi+W6jyDlsI/muLjmiI8g5LyB5Lia5Y+3IOS8geS4muW+ruS/oSDlvIDmlL7lubPlj7Ag5b6u5L+h5pSv5LuYIEpTU0RLIOW+ruS/oeWRqOi+ueetieWFqOW5s+WPsCBXZUNoYXQgU0RLIGZvciBDIOS4reaWh+eLrOeri+WNmuWuouWIl+ihqOmrmOaViOeOhyBRUSDmnLrlmajkurrmlK/mjIHlupPmlK/mjIHlrprliLbku7vkvZXmkq3mlL7lmahTREvlkozmjqfliLblsYIgT3BlblBvd2Vy5bel5L2c57uE5pS26ZuG5rGH5oC755qE5Yy76Zmi5byA5pS+5pWw5o2uWHJheSDln7rkuo4gTmdpbngg55qEIFZMRVNTIFhUTFMg5LiA6ZSu5a6J6KOF6ISa5pysIEZsdXR0ZXJEZW1v5ZCI6ZuGIOS7iuWkqeS9oGZ15LqG5ZCX6I6r54OmUHl0aG9uIOS4reaWh0FJ5pWZ5a2m5Lit5Zu954m56ImyIFRhYkJhciDkuIDooYzku6PnoIHlrp7njrAgTG90dGllIOWKqOeUu1RhYkJhciDmlK/mjIHkuK3pl7TluKYg5Y+355qEVGFiQmFy5qC35byPIOiHquW4pue6oueCueinkuaghyDmlK/mjIHliqjmgIHliLfmlrAgRmx1dHRlcuixhueTo+WuouaIt+erryBBd2Vzb21lIEZsdXR0ZXIgUHJvamVjdCDlhajnvZHmnIAxICXov5jljp/osYbnk6PlrqLmiLfnq68g6aaW6aG1IOS5puW9semfsyDlsI/nu4Qg5biC6ZuG5Y+K5Liq5Lq65Lit5b+DIOS4gOS4quS4jeaLiSBpbWcgeHV2aXAgdG9wIGRvdXlhZGVtbyBtcDQg5Z+65LqOU3ByaW5nQ2xvdWQyIDHnmoTlvq7mnI3liqHlvIDlj5HohJrmiYvmnrYg5pW05ZCI5LqG562JIOacjeWKoeayu+eQhuaWuemdouW8leWFpeetiSDorqnpobnnm67lvIDlj5Hlv6vpgJ/ov5vlhaXkuJrliqHlvIDlj5Eg6ICM5LiN6ZyA6L+H5aSa5pe26Ze06Iqx6LS55Zyo5p625p6E5pCt5bu65LiKIOaMgee7reabtOaWsOS4reWfuuS6jiBWdWUyIOWSjCBFQ2hhcnRzIOWwgeijheeahOWbvuihqOe7hOS7tiBTU1Ig5Y675bm/5ZGKQUNM6KeE5YiZIFNT5a6M5pW0R0ZXTGlzdOinhOWImSBDbGFzaOinhOWImeeijueJhyBUZWxlZ3JhbemikemBk+iuoumYheWcsOWdgOWSjOaIkeS4gOatpeatpemDqOe9siBrdWJlcm5ldGVzIOmbhue+pOaQnOmbhiDmlbTnkIYg57u05oqk5a6e55So6KeE5YiZIOS4reaWh+iHqueEtuivreiogOWkhOeQhuebuOWFs+i1hOaWmeWfuuS6jlNPQeaetuaehOeahOWIhuW4g+W8j+eUteWVhui0reeJqeWVhuWfjiDliY3lkI7nq6/liIbnprsg5YmN5Y+w5ZWG5Z+OIOWFqOWutuahtiDlkI7lj7DnrqHnkIbns7vnu5/nrYlXaGF0IGhhcHBlbnMgd2hlbiDnmoTkuK3mlofnv7vor5Eg5Y6f5LuT5bqTUU1VSSBpT1Mg6Ie05Yqb5LqO5o+Q6auY6aG555uuIFVJIOW8gOWPkeaViOeOh+eahOino+WGs+aWueahiOaWsOWei+WGoOeKtueXheavkumYsueWq+S/oeaBr+aUtumbhuW5s+WPsOWRiuWIq+aer+eHpSDoh7Tlipvkuo7miZPpgKAgUHl0aG9uIOWunueUqOWwj+S+i+WtkOWcqOe6v+WItuS9nCBzb3JyeSDkuLrmiYDmrLLkuLog55qEZ2lmTm9kZWpz5a2m5Lmg56yU6K6w5Lul5Y+K57uP6aqM5oC757uTIOWFrOS8l+WPtyDnqIvluo/njL/lsI/ljaEg5p2O5a6P5q+FIOacuuWZqOWtpuS5oCDnrJTorrAg5Zyo57q/6ZiF6K+75Zyw5Z2AIFZ1ZSBqcyDmupDnoIHliIbmnpBW6YOo6JC9IFZ1ZSBTcHJpbmdCb2905a6e546w55qE5aSa55So5oi35Y2a5a6i566h55CG5bmz5Y+wIEFuZHJvaWQgU2lnbmF0dXJlIFYyIFNjaGVtZeetvuWQjeS4i+eahOaWsOS4gOS7o+a4oOmBk+WMheaJk+WMheelnuWZqEF1dG9zY3JvbGwgQmFubmVyIOaXoOmZkOW+queOr+WbvueJhyDmloflrZfova7mkq3lmagg5aSa56eN57yW56iL6K+t6KiA5a6e546wIExlZXRDb2RlIOWJkeaMhyBPZmZlciDnrKwgMiDniYgg56iL5bqP5ZGY6Z2i6K+V6YeR5YW4IOesrCA2IOeJiCDpopjop6PkuIDlpZfpq5jotKjph4/nmoTlvq7kv6HlsI/nqIvluo8gVUkg57uE5Lu25bqT6aOe5qGoIOWumOaWueaooeWei+W6kyDljIXlkKvlpJrnp43lrabmnK/liY3msr/lkozlt6XkuJrlnLrmma/pqozor4HnmoTmt7HluqblrabkuaDmqKHlnosg5Lit5paHIFB5dGhvbiDnrJTorrDkuJPpl6jkuLrliJrlvIDlp4vliLfpopjnmoTlkIzlrablh4blpIfnmoTnrpfms5Xln7rlnLAg5rKh5pyJ5pyA57uG5Y+q5pyJ5pu057uGIOeri+W/l+eUqOWKqOeUu+WwhuaZpua2qemavuaHgueahOeul+azleivtOeahOmAmuS/l+aYk+aHgiDniYjlhaXpl6jlrp7kvovku6PnoIEg5a6e5oiY5pWZ56iLIOaYr+S4gOS4qumrmOaAp+iDveS4lOS9juaNn+iAl+eahCBnb3JvdXRpbmUg5rGgIENWUFIgMiAyMSDorrrmloflkozlvIDmupDpobnnm67lkIjpm4bmnIkg5pyJICBQeXRob27ov5vpmLYgSW50ZXJtZWRpYXRlIFB5dGhvbiDkuK3mlofniYgg5py65Zmo5Lq66KeG6KeJIOenu+WKqOacuuWZqOS6uiBWUyBTTEFNIE9SQiBTTEFNMiDmt7HluqblrabkuaDnm67moIfmo4DmtYsgeW9sb3YzIOihjOS4uuajgOa1iyBvcGVuY3YgUENMIOacuuWZqOWtpuS5oCDml6Dkurrpqb7pqbblkI7lj7DnrqHnkIbns7vnu5/op6PlhrPmlrnmoYjliJvlu7rlnKjnur/or77nqIsg5a2m5pyv566A5Y6G5oiW5Yid5Yib572R56uZICBDaHJvbWXmj5Lku7blvIDlj5HlhajmlLvnlaUg6YWN5aWX5a6M5pW0RGVtbyDmrKLov45jbG9uZeS9k+mqjFFVQU5UQVhJUyDmlK/mjIHku7vliqHosIPluqYg5YiG5biD5byP6YOo572y55qEIOiCoeelqCDmnJ/otKcg5pyf5p2DIOa4r+iCoSDomZrmi5/otKfluIEg5pWw5o2uIOWbnua1iyDmqKHmi58g5Lqk5piTIOWPr+inhuWMliDlpJrotKbmiLcg57qv5pys5Zyw6YeP5YyW6Kej5Yaz5pa55qGI5b6u5L+h6LCD6K+VIOWQhOenjVdlYlZpZXfmoLflvI/osIPor5Ug5omL5py65rWP6KeI5Zmo55qE6aG16Z2i55yf5py66LCD6K+VIOS+v+aNt+eahOi/nOeoi+iwg+ivleaJi+acuumhtemdoiDmipPljIXlt6Xlhbcg5pSv5oyBIEhUVFBTIOaXoOmcgFVTQui/nuaOpeiuvuWkhyByaWNoIHRleHQg5a+M5paH5pys57yW6L6R5ZmoIOaxieWtl+aLvOmfsyBow6BuIHrDrCBwxKtuIHnEq27pnaLlkJHlvIDlj5HkurrlkZjmorPnkIbnmoTku6PnoIHlronlhajmjIfljZfku6Xmkrjku6PnoIHnmoTlvaLlvI/lrabkuaBQeXRob27mj5DkvpvlkIzoirHpobrlrqLmiLfnq68g5Zu96YeRIOWNjuazsOWuouaIt+erryDpm6rnkIPnmoTln7rph5Eg6IKh56Wo6Ieq5Yqo56iL5bqP5YyW5Lqk5piT5Lul5Y+K6Ieq5Yqo5omT5pawIOaUr+aMgei3n+i4qiBqb2lucXVhbnQgcmljZXF1YW50IOaooeaLn+S6pOaYkyDlkowg5a6e55uY6Zuq55CD57uE5ZCIIOmHj+WMluS6pOaYk+e7hOS7tuaQnOeLkOinhumikSBzb2h1IHR2IFJlZGlz56eB5pyJ5LqR5bmz5Y+wc3ByaW5nIGJvb3TmiZPpgKDmlofku7bmlofmoaPlnKjnur/pooTop4jpobnnm67orqHnrpfmnLrln7rnoYAg6K6h566X5py6572R57ucIOaTjeS9nOezu+e7nyDmlbDmja7lupMgR2l0IOmdouivlemXrumimOWFqOmdouaAu+e7kyDljIXlkKvor6bnu4bnmoRmb2xsb3cgdXAgcXVlc3Rpb27ku6Xlj4rnrZTmoYgg5YWo6YOo6YeH55SoIOmXrumimCDov73pl64g562U5qGIIOeahOW9ouW8jyDljbPmi7/ljbPnlKgg55u05Ye75LqS6IGU572R5aSn5Y6C6Z2i6K+VIOWPr+eUqOS6juaooeaLn+mdouivlSDpnaLor5XliY3lpI3kuaAg55+t5pyf5YaF5b+r6YCf5aSH5oiY6Z2i6K+VIOmmluasvuW+ruS/oSBtYWNPUyDlrqLmiLfnq6/mkqTlm57mi6bmiKrkuI7lpJrlvIB3aW5kb3dzIGtlcm5lbCBleHBsb2l0cyBXaW5kb3dz5bmz5Y+w5o+Q5p2D5ryP5rSe6ZuG5ZCI5p2D6ZmQ566h55CG57O757ufIOmihOiniOWcsOWdgCA0NyAxIDQgNyAxMzggbG9naW5wa3VzZWflpJrpoobln5/kuK3mlofliIbor43lt6XlhbfkuIDmrL7lrozlloTnmoTlronlhajor4TkvLDlt6Xlhbcg5pSv5oyB5bi46KeBIHdlYiDlronlhajpl67popjmiavmj4/lkozoh6rlrprkuYkgcG9jIOS9v+eUqOS5i+WJjeWKoeW/heWFiOmYheivu+aWh+aho+mbtuWPjeWwhOWFqOWKqOaAgUFuZHJvaWTmj5Lku7bmoYbmnrZQeXRob27lhaXpl6jnvZHnu5zniKzomavkuYvnsr7ljY7niYjliIbluIPlvI/phY3nva7nrqHnkIblubPlj7Ag5Lit5paHIGlPUyBNYWMg5byA5Y+R5Y2a5a6i5YiX6KGo5ZGo5b+X5Y2OIOacuuWZqOWtpuS5oCDlj4jnp7Dopb/nk5zkuabmmK/kuIDmnKzovoPkuLrlhajpnaLnmoTkuabnsY0g5Lmm5Lit6K+m57uG5LuL57uN5LqG5py65Zmo5a2m5Lmg6aKG5Z+f5LiN5ZCM57G75Z6L55qE566X5rOVIOS+i+WmgiDnm5HnnaPlrabkuaAg5peg55uR552j5a2m5LmgIOWNiuebkeedo+WtpuS5oCDlvLrljJblrabkuaAg6ZuG5oiQ6ZmN57u0IOeJueW+gemAieaLqeetiSDorrDlvZXkuobmnKzkurrlnKjlrabkuaDov4fnqIvkuK3nmoTnkIbop6PmgJ3ot6/kuI7mianlsZXnn6Xor4bngrkg5biM5pyb5a+55paw5Lq66ZiF6K+76KW/55Oc5Lmm5pyJ5omA5biu5YqpICDlm73lhoXpppbkuKpTcHJpbmcgQ2xvdWTlvq7mnI3liqHljJZSQkFD55qE566h55CG5bmz5Y+wIOaguOW/g+mHh+eUqOWJjeerr+mHh+eUqGQyIGFkbWlu5Lit5Y+w5qGG5p62IOiusOW+l+S4iui+ueeCueS4qnN0YXIg5YWz5rOo5pu05pawQXBhY2hlIEVDaGFydHMgaW5jdWJhdGluZyDnmoTlvq7kv6HlsI/nqIvluo/niYjmnKxDIOi1hOa6kOWkp+WFqOS4reaWh+eJiCDmoIflh4blupMgV2Vi5bqU55So5qGG5p62IOS6uuW3peaZuuiDvSDmlbDmja7lupMg5Zu+54mH5aSE55CGIOacuuWZqOWtpuS5oCDml6Xlv5cg5Luj56CB5YiG5p6Q562JIOeUsSDlvIDmupDliY3lk6gg5ZKMIENQUOW8gOWPkeiAhSDlvq7kv6Hlhazlj7flm6LpmJ/nu7TmiqTmm7TmlrAgc3RhY2tvdmVyZmxvd+S4ikphdmHnm7jlhbPlm57nrZTmlbTnkIbnv7vor5Eg5Z+65LqOR29vZ2xlIEZsdXR0ZXLnmoRXYW5BbmRyb2lk5a6i5oi356uvIOaUr+aMgUFuZHJvaWTlkoxpT1Mg5YyF5ousQkxvQyBSeERhcnQg5Zu96ZmF5YyWIOS4u+mimOiJsiDlkK/liqjpobUg5byV5a+86aG1ICDmnKzku6PnoIHlupPmmK/kvZzogIXlsI/lgoXlk6XlpJrlubTku47kuovkuIDnur/kupLogZTnvZEgSmF2YSDlvIDlj5HnmoTlrabkuaDljobnqIvmioDmnK/msYfmgLsg5peo5Zyo5Li65aSn5a625o+Q5L6b5LiA5Liq5riF5pmw6K+m57uG55qE5a2m5Lmg5pWZ56iLIOS+p+mHjeeCueabtOWAvuWQkee8luWGmUphdmHmoLjlv4PlhoXlrrkg5aaC5p6c5pys5LuT5bqT6IO95Li65oKo5o+Q5L6b5biu5YqpIOivt+e7meS6iOaUr+aMgSDlhbPms6gg54K56LWeIOWIhuS6qyBDIOi1hOa6kOWkp+WFqOS4reaWh+eJiCDljIXmi6zkuoYg5p6E5bu657O757ufIOe8luivkeWZqCDmlbDmja7lupMg5Yqg5a+GIOWIneS4remrmOeahOaVmeeoiyDmjIfljZcg5Lmm57GNIOW6k+etiSAgTkVUIG0zdTggZG93bmxvYWRlciDlvIDmupDnmoTlkb3ku6TooYxtM3U4IEhMUyBkYXNo5LiL6L295ZmoIOaUr+aMgeaZrumAmkFFUyAxMjggQ0JD6Kej5a+GIOWkmue6v+eoiyDoh6rlrprkuYnor7fmsYLlpLTnrYkg5pSv5oyB566A5L2T5Lit5paHIOe5geS9k+S4reaWh+WSjOiLseaWhyBFbmdsaXNoIFN1cHBvcnRlZCDlm73lhoXkvY7ku6PnoIHlubPlj7Dku47kuJrogIXkuqTmtYF0Y2MgdHJhbnNhY3Rpb27mmK9UQ0PlnovkuovliqFqYXZh5a6e546w6K6+6K6h5qih5byPIEdvbGFuZ+WunueOsCDnoJTno6jorr7orqHmqKHlvI8g6K+75Lmm56yU6K6wVnVl5pWw5o2u5Y+v6KeG5YyW57uE5Lu25bqTIOexu+S8vOmYv+mHjERhdGFWIOWkp+Wxj+aVsOaNruWxleekuiDmj5DkvptTVkfnmoTovrnmoYblj4roo4XppbAg5Zu+6KGoIOawtOS9jeWbviDpo57nur/lm77nrYnnu4Tku7Yg566A5Y2V5piT55SoIOmVv+acn+abtOaWsCBSZWFjdOeJiOW3suWPkeW4gyDoh6rlt7HliqjmiYvlgZrogYrlpKnmnLrlmajkurrmlZnnqIsgUmVjeWNsZXJWaWV35L6n5ruR6I+c5Y2VIEl0ZW3mi5bmi70g5ruR5Yqo5Yig6ZmkSXRlbSDoh6rliqjliqDovb3mm7TlpJogSGVhZGVyVmlldyBGb290ZXJWaWV3IEl0ZW3liIbnu4Tpu4/otLQg6IW+6K6v54mp6IGU572R57uI56uv5pON5L2c57O757uf5LiA5Liq5bCP5benIOi9u+mHj+eahOa1j+iniOWZqOWGheaguCDnlKjmnaXlj5bku6N3a2XlkoxsaWJjZWbljIXlkKvnvo7popznrYk0IOS9meenjeWunuaXtua7pOmVnOebuOacuiDlj6/mi43nhacg5b2V5YOPIOWbvueJh+S/ruaUuXNwcmluZ2Jvb3Qg5qGG5p625LiO5YW25a6D57uE5Lu257uT5ZCI5aaC562J55So5rex5bqm5a2m5Lmg5a+55a+56IGUICDmioDmnK/pnaLor5Xlv4XlpIfln7rnoYDnn6Xor4YgTGVldGNvZGUg6K6h566X5py65pON5L2c57O757ufIOiuoeeul+acuue9kee7nCDns7vnu5/orr7orqEgSmF2YeWtpuS5oCDpnaLor5XmjIfljZcg5LiA5Lu95ra155uW5aSn6YOo5YiGIEphdmEg56iL5bqP5ZGY5omA6ZyA6KaB5o6M5o+h55qE5qC45b+D55+l6K+GIOWHhuWkhyBKYXZhIOmdouivlSDpppbpgIkgSmF2YUd1aWRlIOeUqOWKqOeUu+eahOW9ouW8j+WRiOeOsOino0xlZXRDb2Rl6aKY55uu55qE5oCd6LevIOS6kuiBlOe9kSBKYXZhIOW3peeoi+W4iOi/m+mYtuefpeivhuWujOWFqOaJq+ebsiDmtrXnm5bpq5jlubblj5Eg5YiG5biD5byPIOmrmOWPr+eUqCDlvq7mnI3liqEg5rW36YeP5pWw5o2u5aSE55CG562J6aKG5Z+f55+l6K+GbWFsbOmhueebruaYr+S4gOWll+eUteWVhuezu+e7nyDljIXmi6zliY3lj7DllYbln47ns7vnu5/lj4rlkI7lj7DnrqHnkIbns7vnu58g5Z+65LqOU3ByaW5nQm9vdCBNeUJhdGlz5a6e546wIOmHh+eUqERvY2tlcuWuueWZqOWMlumDqOe9siDliY3lj7DllYbln47ns7vnu5/ljIXlkKvpppbpobXpl6jmiLcg5ZWG5ZOB5o6o6I2QIOWVhuWTgeaQnOe0oiDllYblk4HlsZXnpLog6LSt54mp6L2mIOiuouWNlea1geeoiyDkvJrlkZjkuK3lv4Mg5a6i5oi35pyN5YqhIOW4ruWKqeS4reW/g+etieaooeWdlyDlkI7lj7DnrqHnkIbns7vnu5/ljIXlkKvllYblk4HnrqHnkIYg6K6i5Y2V566h55CGIOS8muWRmOeuoeeQhiDkv4PplIDnrqHnkIYg6L+Q6JCl566h55CGIOWGheWuueeuoeeQhiDnu5/orqHmiqXooagg6LSi5Yqh566h55CGIOadg+mZkOeuoeeQhiDorr7nva7nrYnmqKHlnZcgIEdpdEh1YuS4reaWh+aOkuihjOamnCDluK7liqnkvaDlj5HnjrDpq5jliIbkvJjnp4DkuK3mlofpobnnm64g5pu06auY5pWI5Zyw5ZC45pS25Zu95Lq655qE5LyY56eA57uP6aqM5oiQ5p6cIOamnOWNleavj+WRqOabtOaWsOS4gOasoSDmlazor7flhbPms6ggIOeul+azlemdouivlSDnrpfms5Xnn6Xor4Yg6ZKI5a+55bCP55m955qE566X5rOV6K6t57uDIOi/mOWMheaLrCAxIOmYv+mHjCDlrZfoioIg5ru05ru0IOeZvuevh+Wkp+WOgumdoue7j+axh+aAuyAyIOWNg+acrOW8gOa6kOeUteWtkOS5piAzIOeZvuW8oOaAnee7tOWvvOWbviDlj7PkvqfmnaXkuKogc3RhciDlkKcgRW5nbGlzaCB2ZXJzaW9uIHN1cHBvcnRlZCDor4rmlq3liKnlmahBcnRoYXPmlZnnqIsg5oqA5pyv5qCI56S65L6L5Luj56CBIOW/q+mAn+eugOWNleS4iuaJi+aVmeeoiyBodHRw5LiL6L295bel5YW3IOWfuuS6jmh0dHDku6PnkIYg5pSv5oyB5aSa6L+e5o6l5YiG5Z2X5LiL6L296Zi/6YeM5LqR6K6h566X5bmz5Y+w5Zui6Zif5Ye65ZOBIOS4uuebkeaOp+iAjOeUn+eahOaVsOaNruW6k+i/nuaOpeaxoCDkvIHkuJrnuqfkvY7ku6PnoIHlubPlj7Ag5YmN5ZCO56uv5YiG56a75p625p6E5by65aSn55qE5Luj56CB55Sf5oiQ5Zmo6K6p5YmN5ZCO56uv5Luj56CB5LiA6ZSu55Sf5oiQIOaXoOmcgOWGmeS7u+S9leS7o+eggSDlvJXpoobmlrDnmoTlvIDlj5HmqKHlvI9PbmxpbmVDb2Rpbmcg5Luj56CB55Sf5oiQIOaJi+W3pU1FUkdFIOW4ruWKqUphdmHpobnnm67op6PlhrM3ICXph43lpI3lt6XkvZwg6K6p5byA5Y+R5pu05YWz5rOo5Lia5YqhIOaXouiDveW/q+mAn+aPkOmrmOaViOeOhyDluK7liqnlhazlj7joioLnnIHmiJDmnKwg5ZCM5pe25Y+I5LiN5aSx54G15rS75oCnICDkuIvmi4nliLfmlrAg5LiK5ouJ5Yqg6L29IOS6jOe6p+WIt+aWsCDmt5jlrp3kuozmpbzmmbrog73kuIvmi4nliLfmlrDmoYbmnrYg5pSv5oyB6LaK55WM5Zue5by5IOi2iueVjOaLluWKqCDlhbfmnInmnoHlvLrnmoTmianlsZXmgKcg6ZuG5oiQ5LqG5Yeg5Y2B56eN54Kr6YW355qESGVhZGVy5ZKMIEZvb3RlciDor6Xpobnnm67lt7LmiJDlip/pm4bmiJAgYWN0dWF0b3Ig55uR5o6nIGFkbWluIOWPr+inhuWMluebkeaOpyBsb2diYWNrIOaXpeW/lyBhb3BMb2cg6YCa6L+HQU9Q6K6w5b2Vd2Vi6K+35rGC5pel5b+XIOe7n+S4gOW8guW4uOWkhOeQhiBqc29u57qn5Yir5ZKM6aG16Z2i57qn5YirIGZyZWVtYXJrZXIg5qih5p2/5byV5pOOIHRoeW1lbGVhZiDmqKHmnb/lvJXmk44gQmVldGwg5qih5p2/5byV5pOOIEVuam95IOaooeadv+W8leaTjiBKZGJjVGVtcGxhdGUg6YCa55SoSkRCQ+aTjeS9nOaVsOaNruW6kyBKUEEg5by65aSn55qET1JN5qGG5p62IG15YmF0aXMg5by65aSn55qET1JN5qGG5p62IOmAmueUqE1hcHBlciDlv6vpgJ/mk43kvZxNeWJhdGlzIFBhZ2VIZWxwZXIg6YCa55So55qETXliYXRpc+WIhumhteaPkuS7tiBteWJhdGlzIHBsdXMg5b+r6YCf5pON5L2cTXliYXRpcyBCZWV0bFNRTCDlvLrlpKfnmoRPUk3moYbmnrYgdSDlvq7kurrkuovmmK/kuIDkuKrliY3lkI7nq6/liIbnprvnmoTkurrlipvotYTmupDnrqHnkIbns7vnu58g6aG555uu6YeH55SoU3ByaW5nQm9vdCBWdWXlvIDlj5EgIOenkuadgOezu+e7n+iuvuiuoeS4juWunueOsCDkupLogZTnvZHlt6XnqIvluIjov5vpmLbkuI7liIbmnpAgVG8gQmUgVG9wIEphdmFlciBKYXZh5bel56iL5biI5oiQ56We5LmL6Lev5b6q5bqP5riQ6L+bIOWtpuS5oOWNmuWuolNwcmluZ+ezu+WIl+a6kOeggSBtcmJpcmQgY2Plv6vpgJ8g566A5Y2V6YG/5YWNT09N55qEamF2YeWkhOeQhkV4Y2Vs5bel5YW36Zi/6YeM5be05be0IE15U1FMIGJpbmxvZyDlop7ph4/orqLpmIUg5raI6LS557uE5Lu2ICDkuIDmrL7kvJjnp4DnmoTlvIDmupDljZrlrqLlj5HluIPlupTnlKgg5YiG5biD5byP5Lu75Yqh6LCD5bqm5bmz5Y+wWFhMIEpPQiDkuIDmrL7pnaLlkJHms5vliY3nq6/kuqflk4HnoJTlj5HlhajnlJ/lkb3lkajmnJ/nmoTmlYjnjoflubPlj7Ag6Z2i5ZCR5LqR5Y6f55Sf5b6u5pyN5Yqh55qE6auY5Y+v55So5rWB5o6n6Ziy5oqk57uE5Lu2IOinhumikeaSreaUvuWZqOaUr+aMgeW8ueW5lSDlpJbmjILlrZfluZUg5pSv5oyB5ruk6ZWcIOawtOWNsCBnaWbmiKrlm74g54mH5aS05bm/5ZGKIOS4remXtOW5v+WRiiDlpJrkuKrlkIzml7bmkq3mlL4g5pSv5oyB5Z+65pys55qE5ouW5YqoIOWjsOmfsyDkuq7luqbosIPoioIg5pSv5oyB6L655pKt6L6557yT5a2YIOaUr+aMgeinhumikeiHquW4pnJvdGF0aW9u55qE5peL6L2sIDkgMjcg5LmL57G7IOmHjeWKm+aXi+i9rOS4juaJi+WKqOaXi+i9rOeahOWQjOatpeaUr+aMgSDmlK/mjIHliJfooajmkq3mlL4g5YiX6KGo5YWo5bGP5Yqo55S7IOinhumikeWKoOi9vemAn+W6piDliJfooajlsI/nqpflj6PmlK/mjIHmi5bliqgg5Yqo55S75pWI5p6cIOiwg+aVtOavlOS+iyDlpJrliIbovqjnjofliIfmjaIg5pSv5oyB5YiH5o2i5pKt5pS+5ZmoIOi/m+W6puadoeWwj+eql+WPo+mihOiniCDliJfooajliIfmjaLor6bmg4XpobXpnaLml6DnvJ3mkq3mlL4gcnRzcCBjb25jYXQgbXBlZyDlj4jkuIDkuKrlsI/llYbln44gbGl0ZW1hbGwgU3ByaW5nIEJvb3TlkI7nq68gVnVl566h55CG5ZGY5YmN56uvIOW+ruS/oeWwj+eoi+W6j+eUqOaIt+WJjeerryBWdWXnlKjmiLfnp7vliqjnq6/ln7rkuo5TcHJpbmcgU3ByaW5nTVZDIE15YmF0aXPliIbluIPlvI/mlY/mjbflvIDlj5Hns7vnu5/mnrbmnoQg5o+Q5L6b5pW05aWX5YWs5YWx5b6u5pyN5Yqh5pyN5Yqh5qih5Z2XIOmbhuS4readg+mZkOeuoeeQhiDljZXngrnnmbvlvZUg5YaF5a65566h55CGIOaUr+S7mOS4reW/gyDnlKjmiLfnrqHnkIYg5pSv5oyB56ys5LiJ5pa555m75b2VIOW+ruS/oeW5s+WPsCDlrZjlgqjns7vnu58g6YWN572u5Lit5b+DIOaXpeW/l+WIhuaekCDku7vliqHlkozpgJrnn6XnrYkg5pSv5oyB5pyN5Yqh5rK755CGIOebkeaOp+WSjOi/vei4qiDliqrlipvkuLrkuK3lsI/lnovkvIHkuJrmiZPpgKDlhajmlrnkvY1KMkVF5LyB5Lia57qn5byA5Y+R6Kej5Yaz5pa55qGIIOmhueebruWfuuS6jueahOWJjeWQjuerr+WIhuemu+eahOWQjuWPsOeuoeeQhuezu+e7nyDpobnnm67ph4fnlKjliIbmqKHlnZflvIDlj5HmlrnlvI8g5p2D6ZmQ5o6n5Yi26YeH55SoIFJCQUMg5pSv5oyB5pWw5o2u5a2X5YW45LiO5pWw5o2u5p2D6ZmQ566h55CGIOaUr+aMgeS4gOmUrueUn+aIkOWJjeWQjuerr+S7o+eggSDmlK/mjIHliqjmgIHot6/nlLEg5Y+y5LiK5pyA566A5Y2V55qEU3ByaW5nIENsb3Vk5pWZ56iL5rqQ56CBIENBVCDkvZzkuLrmnI3liqHnq6/pobnnm67ln7rnoYDnu4Tku7Yg5o+Q5L6b5LqGIEphdmEgQyBDIE5vZGUganMgUHl0aG9uIEdvIOetieWkmuivreiogOWuouaIt+erryDlt7Lnu4/lnKjnvo7lm6Lngrnor4TnmoTln7rnoYDmnrbmnoTkuK3pl7Tku7bmoYbmnrYgTVZD5qGG5p62IFJQQ+ahhuaetiDmlbDmja7lupPmoYbmnrYg57yT5a2Y5qGG5p62562JIOa2iOaBr+mYn+WIlyDphY3nva7ns7vnu5/nrYkg5rex5bqm6ZuG5oiQIOS4uue+juWboueCueivhOWQhOS4muWKoee6v+aPkOS+m+ezu+e7n+S4sOWvjOeahOaAp+iDveaMh+aghyDlgaXlurfnirblhrUg5a6e5pe25ZGK6K2m562JIHNwcmluZyBib290IOWunui3teWtpuS5oOahiOS+iyDmmK8gc3ByaW5nIGJvb3Qg5Yid5a2m6ICF5Y+K5qC45b+D5oqA5pyv5bep5Zu655qE5pyA5L2z5a6e6Le1IOWPpuWkluWGmeWNmuWuoiDnlKggT3BlbldyaXRlIFNwcmluZyBCb2905Z+656GA5pWZ56iLIFNwcmluZyBCb290IDIgeOeJiOacrOi/nui9veS4rSDluK7liqkgQW5kcm9pZCBBcHAg6L+b6KGM57uE5Lu25YyW5pS56YCg55qE6Lev55Sx5qGG5p62IOaPkOmrmCBBbmRyb2lkIFVJIOW8gOWPkeaViOeOh+eahCBVSSDlupPml7bpl7TpgInmi6nlmagg55yB5biC5Yy65LiJ57qn6IGU5YqoIEx1YmFuIOmygeePreWPr+iDveaYr+acgOaOpei/keW+ruS/oeaci+WPi+WciOeahOWbvueJh+WOi+e8qeeul+azlSBHaXRlZSDmnIDmnInku7flgLzlvIDmupDpobnnm64g5bCP6ICM5YWo6ICM576O55qE56ys5LiJ5pa555m75b2V5byA5rqQ57uE5Lu2IOebruWJjeW3suaUr+aMgUdpdGh1YiBHaXRlZSDlvq7ljZog6ZKJ6ZKJIOeZvuW6piBDb2Rpbmcg6IW+6K6v5LqR5byA5Y+R6ICF5bmz5Y+wIE9TQ2hpbmEg5pSv5LuY5a6dIFFRIOW+ruS/oSDmt5jlrp0gR29vZ2xlIEZhY2Vib29rIOaKlumfsyDpooboi7Eg5bCP57GzIOW+rui9ryDku4rml6XlpLTmnaHkurrkurog5Y2O5Li6IOS8geS4muW+ruS/oSDphbflrrbkuZAgR2l0bGFiIOe+juWboiDppb/kuobkuYgg5o6o54m5IOmjnuS5piDkuqzkuJwg6Zi/6YeM5LqRIOWWnOmprOaLiembhSBBbWF6b24gU2xhY2vlkowgTGluZSDnrYnnrKzkuInmlrnlubPlj7DnmoTmjojmnYPnmbvlvZUgTG9naW4gc28gZWFzeSDku4rml6XlpLTmnaHlsY/luZXpgILphY3mlrnmoYjnu4jmnoHniYgg5LiA5Liq5p6B5L2O5oiQ5pys55qEIEFuZHJvaWQg5bGP5bmV6YCC6YWN5pa55qGIICBCYW5uZXIgMiDmnaXkuoYgQW5kcm9pZOW5v+WRiuWbvueJh+i9ruaSreaOp+S7tiDlhoXpg6jln7rkuo5WaWV3UGFnZXIy5a6e546wIEluZGljYXRvcuWSjFVJ6YO95Y+v5Lul6Ieq5a6a5LmJICDpm7bku6PnoIEg54Ot5pu05pawIOiHquWKqOWMliBPUk0g5bqTIOWQjuerr+aOpeWPo+WSjOaWh+aho+mbtuS7o+eggSDliY3nq68g5a6i5oi356uvIOWumuWItui/lOWbniBKU09OIOeahOaVsOaNruWSjOe7k+aehOS4gOS4qua2teebluWFreS4quS4k+agj+WIhuW4g+W8j+a2iOaBr+mYn+WIlyDliIbluIPlvI/kuovliqHnmoTku5PlupMg5biM5pyb6IOW5Y+L5bCP5omL5LiA5oqWIOWPs+S4iuinkuadpeS4qiBTdGFyIOaEn+aBqSAxIDI0TXliYXRpc+mAmueUqOWIhumhteaPkuS7tk9rR28gMyDpnIfmkrzmnaXooq0g6K+l5bqT5piv5Z+65LqOIOWNj+iuriDlsIHoo4XkuoYgT2tIdHRwIOeahOe9kee7nOivt+axguahhuaetiDmr5QgUmV0cm9maXQg5pu0566A5Y2V5piT55SoIOaUr+aMgSBSeEphdmEgUnhKYXZhMiDmlK/mjIHoh6rlrprkuYnnvJPlrZgg5pSv5oyB5om56YeP5pat54K55LiL6L29566h55CG5ZKM5om56YeP5LiK5Lyg566h55CG5Yqf6IO95ZCrIEZsaW5rIOWFpemXqCDmpoLlv7Ug5Y6f55CGIOWunuaImCDmgKfog73osIPkvJgg5rqQ56CB6Kej5p6Q562J5YaF5a65IOa2ieWPiuetieWGheWuueeahOWtpuS5oOahiOS+iyDov5jmnIkgRmxpbmsg6JC95Zyw5bqU55So55qE5aSn5Z6L6aG555uu5qGI5L6LIFBWVVYg5pel5b+X5a2Y5YKoIOeZvuS6v+aVsOaNruWunuaXtuWOu+mHjSDnm5HmjqflkYroraYg5YiG5LqrIOasoui/juWkp+WutuaUr+aMgeaIkeeahOS4k+agjyDlpKfmlbDmja7lrp7ml7borqHnrpflvJXmk44gRmxpbmsg5a6e5oiY5LiO5oCn6IO95LyY5YyWIOWuieWNk+W5s+WPsOS4iueahEphdmFTY3JpcHToh6rliqjljJblt6XlhbcgIO+4j+S4gOS4quaVtOWQiOS6huWkp+mHj+S4u+a1geW8gOa6kOmhueebrumrmOW6puWPr+mFjee9ruWMlueahCBBbmRyb2lkIE1WUCDlv6vpgJ/pm4bmiJDmoYbmnrYgU3ByaW5n5rqQ56CB6ZiF6K+75aSn5pWw5o2u5YWl6Zeo5oyH5Y2XIGFuZHJvaWQgNCA05Lul5LiK5rKJ5rW45byP54q25oCB5qCP5ZKM5rKJ5rW45byP5a+86Iiq5qCP566h55CGIOmAgumFjeaoquerluWxj+WIh+aNoiDliJjmtbflsY8g6L2v6ZSu55uY5by55Ye6562J6Zeu6aKYIOWPr+S7peS/ruaUueeKtuaAgeagj+Wtl+S9k+minOiJsuWSjOWvvOiIquagj+Wbvuagh+minOiJsiDku6Xlj4rkuI3lj6/kv67mlLnlrZfkvZPpopzoibLmiYvmnLrnmoTpgILphY0g6YCC55So5LqO5LiA5Y+l5Luj56CB6L275p2+5a6e546wIOS7peWPiuWvuWJhcueahOWFtuS7luiuvue9riDor6bop4FSRUFETUUg566A5Lmm6K+35Y+C6ICDIHd3dyBqaWFuc2h1IGNvbSBwIDJhODg0ZTIxMWE2MuS4muWGheS4uuaVsOS4jeWkmuiHtOWKm+S6juaegeiHtOS9k+mqjOeahOi2heW8uuWFqOiHqueglOi3qOW5s+WPsCB3aW5kb3dzIGFuZHJvaWQgaU9TIOa1geWqkuS9k+WGheaguCDpgJrov4fmqKHlnZfljJboh6rnlLHnu4TlkIgg5pSv5oyB5a6e5pe2UlRNUOaOqOa1gSBSVFNQ5o6o5rWBIFJUTVDmkq3mlL7lmaggUlRTUOaSreaUvuWZqCDlvZXlg48g5aSa6Lev5rWB5aqS5L2T6L2s5Y+RIOmfs+inhumikeWvvOaSrSDliqjmgIHop4bpopHlkIjmiJAg6Z+z6aKR5re36Z+zIOebtOaSreS6kuWKqCDlhoXnva7ovbvph4/nuqdSVFNQ5pyN5Yqh562JIOavlOW/q+abtOW/qyDkuJrnlYznnJ/mraPpnaDosLHnmoTotoXkvY7lu7bov5/nm7Tmkq1TREsgMeenkuWGhSDkvY7lu7bov5/mqKHlvI/kuIsyIDQgbXMgRGF0YVjmmK/pmL/ph4zkupFEYXRhV29ya3PmlbDmja7pm4bmiJDnmoTlvIDmupDniYjmnKwgbWFsbOWtpuS5oOaVmeeoiyDmnrbmnoQg5Lia5YqhIOaKgOacr+imgeeCueWFqOaWueS9jeino+aekCBtYWxs6aG555uuIDQgayBzdGFyIOaYr+S4gOWll+eUteWVhuezu+e7nyDkvb/nlKjnjrDpmLbmrrXkuLvmtYHmioDmnK/lrp7njrAg5ra155uW5LqG562J5oqA5pyvIOmHh+eUqERvY2tlcuWuueWZqOWMlumDqOe9siBBbmRyb2lk5byA5rqQ5by55bmV5byV5pOOIOeDiOeEsOW8ueW5leS9vyDvvZ5zcHJpbmcgY2xvdWQgdnVlIG9BdXRoMiDlhajlrrbmobblrp7miJgg5YmN5ZCO56uv5YiG56a75qih5ouf5ZWG5Z+OIOWujOaVtOeahOi0reeJqea1geeoiyDlkI7nq6/ov5DokKXlubPlj7Ag5Y+v5Lul5a6e546w5b+r6YCf5pCt5bu65LyB5Lia57qn5b6u5pyN5Yqh6aG555uuIOaUr+aMgeW+ruS/oeeZu+W9leetieS4ieaWueeZu+W9lSAg5LiA5Liq5Z+65LqOU3ByaW5nIEJvb3QgTXlCYXRpc+eahOenjeWtkOmhueebriDnlKjkuo7lv6vpgJ/mnoTlu7rkuK3lsI/lnotBUEkgUkVTVGZ1bCBBUEnpobnnm64g5by65aSnIOWPr+WumuWItiDmmJPmianlsZXnmoQgVmlld1BhZ2VyIOaMh+ekuuWZqOahhuaetiDmmK/nmoTmnIDkvbPmm7/ku6Plk4Eg5pSv5oyB6KeS5qCHIOabtOaUr+aMgeWcqOmdnlZpZXdQYWdlcuWcuuaZr+S4i+S9v+eUqCDkvb/nlKhoaWRlIHNob3cg5YiH5o2iRnJhZ21lbnTmiJbkvb/nlKhzZSAyN+WkqeaIkOS4ukphdmHlpKfnpZ7lronljZPlrabkuaDnrJTorrAgY2ltIGNyb3NzIElNIOmAgueUqOS6juW8gOWPkeiAheeahOWIhuW4g+W8j+WNs+aXtumAmuiur+ezu+e7n0FuZHJvaWTkuIrkuIDkuKrkvJjpm4Ug5LiH6IO96Ieq5a6a5LmJVUkg5Lu/aU9TIOaUr+aMgeWeguebtCDmsLTlubPmlrnlkJHliIfmjaIg5pSv5oyB5ZGo6KeG5Zu+IOiHquWumuS5ieWRqOi1t+WniyDmgKfog73pq5jmlYjnmoTml6Xljobmjqfku7Yg5pSv5oyB54Ot5o+S5ouU5a6e546w55qEVUnlrprliLYg5pSv5oyB5qCH6K6wIOiHquWumuS5ieminOiJsiDlhpzljoYg6Ieq5a6a5LmJ5pyI6KeG5Zu+5ZCE56eN5pi+56S65qih5byP562JIENhbnZhc+e7mOWItiDpgJ/luqblv6sg5Y2g55So5YaF5a2Y5L2OIOS9oOecn+eahOaDs+S4jeWIsOaXpeWOhuWxheeEtui/mOWPr+S7peWmguatpOS8mOmbhWhzd2ViIGhhyopzIHfJm2Ig5piv5LiA5Liq5Z+65LqOc3ByaW5nIGJvb3QgMiB45byA5Y+RIOmmluS4quS9v+eUqOWFqOWTjeW6lOW8j+e8lueoi+eahOS8geS4mue6p+WQjuWPsOeuoeeQhuezu+e7n+WfuuehgOmhueebriAgbmV3YmVlIG1hbGwg6aG555uuIOaWsOicguWVhuWfjiDmmK/kuIDlpZfnlLXllYbns7vnu58g5YyF5ousIG5ld2JlZSBtYWxsIOWVhuWfjuezu+e7n+WPiiBuZXdiZWUgbWFsbCBhZG1pbiDllYbln47lkI7lj7DnrqHnkIbns7vnu58g5Z+65LqOIFNwcmluZyBCb290IDIgWCDlj4rnm7jlhbPmioDmnK/moIjlvIDlj5Eg5YmN5Y+w5ZWG5Z+O57O757uf5YyF5ZCr6aaW6aG16Zeo5oi3IOWVhuWTgeWIhuexuyDmlrDlk4HkuIrnur8g6aaW6aG16L2u5pKtIOWVhuWTgeaOqOiNkCDllYblk4HmkJzntKIg5ZWG5ZOB5bGV56S6IOi0reeJqei9piDorqLljZXnu5Pnrpcg6K6i5Y2V5rWB56iLIOS4quS6uuiuouWNleeuoeeQhiDkvJrlkZjkuK3lv4Mg5biu5Yqp5Lit5b+D562J5qih5Z2XIOWQjuWPsOeuoeeQhuezu+e7n+WMheWQq+aVsOaNrumdouadvyDova7mkq3lm77nrqHnkIYg5ZWG5ZOB566h55CGIOiuouWNleeuoeeQhiDkvJrlkZjnrqHnkIYg5YiG57G7566h55CGIOiuvue9ruetieaooeWdlyBtYWxsIHN3YXJt5piv5LiA5aWX5b6u5pyN5Yqh5ZWG5Z+O57O757ufIOmHh+eUqOS6huetieaguOW/g+aKgOacryDlkIzml7bmj5Dkvpvkuobln7rkuo5WdWXnmoTnrqHnkIblkI7lj7Dmlrnkvr/lv6vpgJ/mkK3lu7rns7vnu58gbWFsbCBzd2FybeWcqOeUteWVhuS4muWKoeeahOWfuuehgOmbhuaIkOS6huazqOWGjOS4reW/gyDphY3nva7kuK3lv4Mg55uR5o6n5Lit5b+DIOe9keWFs+etieezu+e7n+WKn+iDvSDmlofmoaPpvZDlhagg6ZmE5bim5YWo5aWXU3ByaW5nIENsb3Vk5pWZ56iLIOmYheivu+aYr+S4gOasvuWPr+S7peiHquWumuS5ieadpea6kOmYheivu+e9kee7nOWGheWuueeahOW3peWFtyDkuLrlub/lpKfnvZHnu5zmloflrabniLHlpb3ogIXmj5DkvpvkuIDnp43mlrnkvr8g5b+r5o236IiS6YCC55qE6K+V6K+75L2T6aqMIFNwcmluZyBDbG91ZOWfuuehgOaVmeeoiyDmjIHnu63ov57ovb3mm7TmlrDkuK3pmL/ph4zlt7Tlt7TliIbluIPlvI/mlbDmja7lupPlkIzmraXns7vnu58g6Kej5Yaz5Lit576O5byC5Zyw5py65oi/ICDln7rkuo7osLfmrYzmnIDmlrBBQUPmnrbmnoQgTVZWTeiuvuiuoeaooeW8j+eahOS4gOWll+W/q+mAn+W8gOWPkeW6kyDmlbTlkIhPa1J4SmF2YSBSZXRyb2ZpdCBHbGlkZeetieS4u+a1geaooeWdlyDmu6HotrPml6XluLjlvIDlj5HpnIDmsYIg5L2/55So6K+l5qGG5p625Y+v5Lul5b+r6YCf5byA5Y+R5LiA5Liq6auY6LSo6YePIOaYk+e7tOaKpOeahEFuZHJvaWTlupTnlKgg5Z+65LqOU3ByaW5nQ2xvdWQyIDHnmoTlvq7mnI3liqHlvIDlj5HohJrmiYvmnrYg5pW05ZCI5LqG562JIOacjeWKoeayu+eQhuaWuemdouW8leWFpeetiSDorqnpobnnm67lvIDlj5Hlv6vpgJ/ov5vlhaXkuJrliqHlvIDlj5Eg6ICM5LiN6ZyA6L+H5aSa5pe26Ze06Iqx6LS55Zyo5p625p6E5pCt5bu65LiKIOaMgee7reabtOaWsOS4reWfuuS6jlNPQeaetuaehOeahOWIhuW4g+W8j+eUteWVhui0reeJqeWVhuWfjiDliY3lkI7nq6/liIbnprsg5YmN5Y+w5ZWG5Z+OIOWFqOWutuahtiDlkI7lj7DnrqHnkIbns7vnu5/nrYnmmK8g6Zq+5b6X5LiA6KeBIOeahCBKZXRwYWNrIE1WVk0g5pyA5L2z5a6e6Le1IOWcqCDku6XnroDpqa3nuYEg55qE5Luj56CB5LitIOWvuSDop4blm77mjqfliLblmagg5LmD6IezIOagh+WHhuWMluW8gOWPkeaooeW8jyDlvaLmiJDmraPnoa4g5rex5YWl55qE55CG6KejIFbpg6jokL0gVnVlIFNwcmluZ0Jvb3Tlrp7njrDnmoTlpJrnlKjmiLfljZrlrqLnrqHnkIblubPlj7AgQW5kcm9pZCBTaWduYXR1cmUgVjIgU2NoZW1l562+5ZCN5LiL55qE5paw5LiA5Luj5rig6YGT5YyF5omT5YyF56We5Zmo5Y2z5pe26YCa6K6vIElNIOezu+e7n+Wkmuenjee8lueoi+ivreiogOWunueOsCBMZWV0Q29kZSDliZHmjIcgT2ZmZXIg56ysIDIg54mIIOeoi+W6j+WRmOmdouivlemHkeWFuCDnrKwgNiDniYgg6aKY6Kej5LiT6Zeo5Li65Yia5byA5aeL5Yi36aKY55qE5ZCM5a2m5YeG5aSH55qE566X5rOV5Z+65ZywIOayoeacieacgOe7huWPquacieabtOe7hiDnq4vlv5fnlKjliqjnlLvlsIbmmabmtqnpmr7mh4LnmoTnrpfms5Xor7TnmoTpgJrkv5fmmJPmh4IgYW5zauWIhuivjSBpY3TnmoTnnJ/mraNqYXZh5a6e546wIOWIhuivjeaViOaenOmAn+W6pumDvei2hei/h+W8gOa6kOeJiOeahGljdCDkuK3mlofliIbor40g5Lq65ZCN6K+G5YirIOivjeaAp+agh+azqCDnlKjmiLfoh6rlrprkuYnor43lhbggYm9vayDku7vpmIUg572R57uc5bCP6K+06ZiF6K+75ZmoIDNE57+76aG15pWI5p6cIHR4dCBwZGYgZXB1YuS5puexjemYheivuyBXaWZp5Lyg5LmmIExlZXRDb2Rl5Yi36aKY6K6w5b2V5LiO6Z2i6K+V5pW055CGbXliYXRpcyBnZW5lcmF0b3LnlYzpnaLlt6Xlhbcg6K6p5L2g55Sf5oiQ5Luj56CB5pu0566A5Y2V5pu05b+r5o23U3ByaW5nIENsb3VkIOWtpuS5oOahiOS+iyDmnI3liqHlj5HnjrAg5pyN5Yqh5rK755CGIOmTvui3r+i/vei4qiDmnI3liqHnm5HmjqfnrYkgWFBvcHVwMiDniYjmnKzph43no4XmnaXooq0gMuWAjeS7peS4iuaAp+iDveaPkOWNhyDluKbmnaXlj6/op4LnmoTliqjnlLvmgKfog73kvJjljJblkozkuqTkupLnu4boioLnmoTmj5DljYcg5Yqf6IO95by65aSnIOS6pOS6kuS8mOmbhSDliqjnlLvkuJ3mu5HnmoTpgJrnlKjlvLnnqpcg5Y+v5Lul5pu/5Luj562J57uE5Lu2IOiHquW4puWNgeWHoOenjeaViOaenOiJr+WlveeahOWKqOeUuyDmlK/mjIHlrozlhajnmoRVSeWSjOWKqOeUu+iHquWumuS5ieaQnOeLkOinhumikSBzb2h1IHR2IFJlZGlz56eB5pyJ5LqR5bmz5Y+wc3ByaW5nIGJvb3TmiZPpgKDmlofku7bmlofmoaPlnKjnur/pooTop4jpobnnm67mnYPpmZDnrqHnkIbns7vnu58g6aKE6KeI5Zyw5Z2AIDQ3IDEgNCA3IDEzOCBsb2dpbumbtuWPjeWwhOWFqOWKqOaAgUFuZHJvaWTmj5Lku7bmoYbmnrbliIbluIPlvI/phY3nva7nrqHnkIblubPlj7Ag6YCa55SoIElNIOiBiuWkqSBVSSDnu4Tku7Yg5bey57uP5ZCM5pe25pSv5oyBIEFuZHJvaWQgaU9TIFJOIOaJi+aKiuaJi+aVmeS9oOaVtOWQiOacgOS8mOmbhVNTTeahhuaetiBTcHJpbmdNVkMgU3ByaW5nIE15QmF0aXPmjaLogqTmoYbmnrYg5p6B5L2O55qE5a2m5Lmg5oiQ5pysIOaegeWlveeahOeUqOaIt+S9k+mqjCDkuIDooYwg5Luj56CB5bCx5Y+v5Lul5a6e546w5o2i6IKkIOS9oOWAvOW+l+aLpeaciSAgSlZNIOW6leWxguWOn+eQhuacgOWFqOefpeivhuaAu+e7kyDlm73lhoXpppbkuKpTcHJpbmcgQ2xvdWTlvq7mnI3liqHljJZSQkFD55qE566h55CG5bmz5Y+wIOaguOW/g+mHh+eUqOWJjeerr+mHh+eUqGQyIGFkbWlu5Lit5Y+w5qGG5p62IOiusOW+l+S4iui+ueeCueS4qnN0YXIg5YWz5rOo5pu05pawdGNjIHRyYW5zYWN0aW9u5pivVEND5Z6L5LqL5YqhamF2YeWunueOsCBSZWN5Y2xlclZpZXfkvqfmu5Hoj5zljZUgSXRlbeaLluaLvSDmu5HliqjliKDpmaRJdGVtIOiHquWKqOWKoOi9veabtOWkmiBIZWFkZXJWaWV3IEZvb3RlclZpZXcgSXRlbeWIhue7hOm7j+i0tCDljIXlkKvnvo7popznrYk0IOS9meenjeWunuaXtua7pOmVnOebuOacuiDlj6/mi43nhacg5b2V5YOPIOWbvueJh+S/ruaUuXNwcmluZ2Jvb3Qg5qGG5p625LiO5YW25a6D57uE5Lu257uT5ZCI5aaC562J5a6J5Y2T6YCJ5oup5Zmo57G75bqTIOWMheaLrOaXpeacn+WPiuaXtumXtOmAieaLqeWZqCDlj6/nlKjkuo7lh7rnlJ/ml6XmnJ8g6JCl5Lia5pe26Ze0562JIOWNlemhuemAieaLqeWZqCDlj6/nlKjkuo7mgKfliKsg5rCR5pePIOiBjOS4miDlrabljoYg5pif5bqn562JIOS6jOS4iee6p+iBlOWKqOmAieaLqeWZqCDlj6/nlKjkuo7ovabniYzlj7cg5Z+66YeR5a6a5oqV5pel5pyf562JIOWfjuW4guWcsOWdgOmAieaLqeWZqCDliIbnnIHnuqcg5Zyw5biC57qn5Y+K5Yy65Y6/57qnIOaVsOWtl+mAieaLqeWZqCDlj6/nlKjkuo7lubTpvoQg6Lqr6auYIOS9k+mHjSDmuKnluqbnrYkg5pel5Y6G6YCJ5pel5pyf5oup5ZmoIOWPr+eUqOS6jumFkuW6l+WPiuacuuelqOmihOWumuaXpeacnyDpopzoibLpgInmi6nlmagg5paH5Lu25Y+K55uu5b2V6YCJ5oup5Zmo562JIEphdmHlt6XnqIvluIjpnaLor5XlpI3kuaDmjIfljZcg5pys5LuT5bqT5ra155uW5aSn6YOo5YiGSmF2Yeeoi+W6j+WRmOaJgOmcgOimgeaOjOaPoeeahOaguOW/g+efpeivhiDmlbTlkIjkuobkupLogZTnvZHkuIrnmoTlvojlpJrkvJjotKhKYXZh5oqA5pyv5paH56ugIOWKm+axguaJk+mAoOS4uuacgOWujOaVtOacgOWunueUqOeahEphdmHlvIDlj5HogIXlrabkuaDmjIfljZcg5aaC5p6c5a+55L2g5pyJ5biu5YqpIOe7meS4qnN0YXLlkYror4nmiJHlkKcg6LCi6LCiICBBbmRyb2lkIE1WUCDlv6vpgJ/lvIDlj5HmoYbmnrYg5YGa5Zu95YaFIOekuuS+i+acgOWFqOmdoiDms6jph4rmnIDor6bnu4Yg5L2/55So5pyA566A5Y2VIOS7o+eggeacgOS4peiwqCDnmoQgQW5kcm9pZCDlvIDmupAgVUkg5qGG5p625Yeg6KGM5Luj56CB5b+r6YCf6ZuG5oiQ5LqM57u056CB5omr5o+P5Yqf6IO9TWV0ZXJTcGhlcmUg5piv5LiA56uZ5byP5byA5rqQ5oyB57ut5rWL6K+V5bmz5Y+wIOa2teeblua1i+ivlei3n+i4qiDmjqXlj6PmtYvor5Ug5oCn6IO95rWL6K+VIOWboumYn+WNj+S9nOetieWKn+iDvSDlhajpnaLlhbzlrrkgSk1ldGVyIFBvc3RtYW4gU3dhZ2dlciDnrYnlvIDmupAg5Li75rWB5qCH5YeGIOiusOW9leWQhOenjeWtpuS5oOeslOiusCDnrpfms5UgSmF2YSDmlbDmja7lupMg5bm25Y+RIOS4i+S4gOS7o0FuZHJvaWTmiZPljIXlt6XlhbcgMSDkuKrmuKDpgZPljIXlj6rpnIDopoExIOenkumSn+iKi+mBkyBtYWxsIOWVhuWfjiDln7rkuo7lvq7mnI3liqHnmoTmgJ3mg7Mg5p6E5bu65ZyoIEIyQyDnlLXllYblnLrmma/kuIvnmoTpobnnm67lrp7miJgg5qC45b+D5oqA5pyv5qCIIOaYryBTcHJpbmcgQm9vdCBEdWJibyDmnKrmnaUg5Lya6YeN5p6E5oiQIFNwcmluZyBDbG91ZCBBbGliYWJhIEFuZHJvaWQg5LiH6IO955qE562JIOaUr+aMgeWkmuenjUl0ZW3nsbvlnovnmoTmg4XlhrUgbGFucHJveHnmmK/kuIDkuKrlsIblsYDln5/nvZHkuKrkurrnlLXohJEg5pyN5Yqh5Zmo5Luj55CG5Yiw5YWs572R55qE5YaF572R56m/6YCP5bel5YW3IOaUr+aMgXRjcOa1gemHj+i9rOWPkSDlj6/mlK/mjIHku7vkvZV0Y3DkuIrlsYLljY/orq4g6K6/6Zeu5YaF572R572R56uZIOacrOWcsOaUr+S7mOaOpeWPo+iwg+ivlSBzc2jorr/pl64g6L+c56iL5qGM6Z2iIOebruWJjeW4gumdouS4iuaPkOS+m+exu+S8vOacjeWKoeeahOacieiKseeUn+WjsyBUZWFtVmlldyBHb1RvTXlDbG91ZOetieetiSDkvYbopoHkvb/nlKjnrKzkuInmlrnnmoTlhaznvZHmnI3liqHlmajlsLHlv4XpobvkuLrnrKzkuInmlrnku5jotLkg5bm25LiU6L+Z5Lqb5pyN5Yqh6YO95pyJ5ZCE56eN5ZCE5qC355qE6ZmQ5Yi2IOatpOWkliDnlLHkuo7mlbDmja7ljIXkvJrmtYHnu4/nrKzkuInmlrkg5Zug5q2k5a+55pWw5o2u5a6J5YWo5Lmf5piv5LiA5aSn6ZqQ5oKjIOaKgOacr+S6pOa1gVFR576kIDEgNjc0MjQzMyDmm7TkvJjpm4XnmoTpqb7ovabkvZPpqozkuIvovb3lj6/ku6XlvojnroDljZUg77iPIOS6kemYhSDkuIDmrL7ln7rkuo7nvZHmmJPkupHpn7PkuZBVSSDkvb/nlKjnjqnmnrbmnoTlvIDlj5HnmoTnrKblkIhHb29nbGUgTWF0ZXJpYWwgRGVzaWdu55qEQW5kcm9pZOWuouaIt+err+W8gOa6kOeahCBNYXRlcmlhbCBEZXNpZ24g6LGG55Oj5a6i5oi356uv5LiA5qy+6ZKI5a+557O757ufUG9wdXBXaW5kb3fkvJjljJbnmoRQb3B1cOW6kyDlip/og73lvLrlpKcg5pSv5oyB6IOM5pmv5qih57OKIOS9v+eUqOeugOWNlSDkvaDkvJrniLHkuIrku5bnmoQgUExEcm9pZFBsYXllciDmmK/kuIPniZvmjqjlh7rnmoTkuIDmrL7lhY3otLnnmoTpgILnlKjkuo4gQW5kcm9pZCDlubPlj7DnmoTmkq3mlL7lmaggU0RLIOmHh+eUqOWFqOiHqueglOeahOi3qOW5s+WPsOaSreaUvuWGheaguCDmi6XmnInkuLDlr4znmoTlip/og73lkozkvJjlvILnmoTmgKfog70g5Y+v6auY5bqm5a6a5Yi25YyW5ZKM5LqM5qyh5byA5Y+RIOivpemhueebruW3suWBnOatoue7tOaKpCA5IFBvcm4gQW5kcm9pZCDlrqLmiLfnq68g56qB56C05ri45a6i5q+P5aSp6KeC55yLMSDmrKHop4bpopHnmoTpmZDliLYg6L+Y5Y+v5Lul5LiL6L296KeG6aKRIO+4j+iTnee7vyDngbDluqYg6Lev55SxIOmZkOa1gSDnhpTmlq0g6ZmN57qnIOmalOemuyDov73ouKog5rWB6YeP5p+T6ImyIOaVhemanOi9rOenu+S4gOacrOWFs+S6juaOkuW6j+eul+azleeahCBHaXRCb29rIOWcqOe6v+S5puexjSDljYHlpKfnu4/lhbjmjpLluo/nrpfms5Ug5aSa6K+t6KiA5a6e546wIOWkmuenjeS4i+aLieWIt+aWsOaViOaenCDkuIrmi4nliqDovb3mm7TlpJog5Y+v6YWN572u6Ieq5a6a5LmJ5aS06YOo5bm/5ZGK5L2N5a6M5YWo5Lu/5b6u5L+h55qE5Zu+54mH6YCJ5oupIOW5tuS4lOaPkOS+m+S6huWkmuenjeWbvueJh+WKoOi9veaOpeWPoyDpgInmi6nlm77niYflkI7lj6/ku6Xml4vovawg5Y+v5Lul6KOB5Ymq5oiQ55+p5b2i5oiW5ZyG5b2iIOWPr+S7pemFjee9ruWQhOenjeWFtuS7lueahOWPguaVsFNvbG9QaSDoh6rliqjljJbmtYvor5Xlt6XlhbfpvpnmnpzmlK/ku5jns7vnu58gcm9uY29vIHBheSDmmK/lm73lhoXpppbmrL7lvIDmupDnmoTkupLogZTnvZHmlK/ku5jns7vnu58g5oul5pyJ54us56uL55qE6LSm5oi35L2T57O7IOeUqOaIt+S9k+ezuyDmlK/ku5jmjqXlhaXkvZPns7sg5pSv5LuY5Lqk5piT5L2T57O7IOWvuei0pua4hee7k+eul+S9k+ezuyDnm67moIfmmK/miZPpgKDkuIDmrL7pm4bmiJDkuLvmtYHmlK/ku5jmlrnlvI/kuJTovbvph4/mmJPnlKjnmoTmlK/ku5jmlLbmrL7ns7vnu58g5ruh6Laz5LqS6IGU572R5Lia5Yqh57O757uf5omT6YCa5pSv5LuY6YCa6YGT5a6e546w5pSv5LuY5pS25qy+5ZKM5Lia5Yqh6LWE6YeR566h55CG562J5Yqf6IO9IOmUruebmOmdouadv+WGsueqgSDluIPlsYDpl6rliqjlpITnkIbmlrnmoYggIOWSleazoeWtpumZouWunuaImOmhueebriDln7rkuo5TcHJpbmdCb290IER1YmJv5p6E5bu655qE55S15ZWG5bmz5Y+wIOW+ruacjeWKoeaetuaehCDllYbln44g55S15ZWGIOW+ruacjeWKoSDpq5jlubblj5Ega2Fma2EgRWxhc3RpY3NlYXJjaOWBnOi9puWcuuezu+e7n+a6kOeggSDlgZzovablnLrlsI/nqIvluo8g5pm66IO95YGc6L2mIFBhcmtpbmcgc3lzdGVtIOWKn+iDveS7i+e7jSDikaDlhbzlrrnluILpnaLkuIrkuLvmtYHnmoTlpJrlrrbnm7jmnLog55CG6K665LiK5YW85a655omA5pyJ56Gs5Lu2IOWPr+eBtea0u+aJqeWxlSDikaHnm7jmnLror4bliKvlkI7mlbDmja7oh6rliqjkuIrkvKDliLDkupHnq6/lubborrDlvZUg5qCh6aqM55u45py65ZSv5LiAaWTlkoznoazku7bluo/liJflj7cg6Ziy5q2i6Z2e5rOV5pWw5o2u5b2V5YWlIOKRoueUqOaIt+aJi+acuuafpeivouWBnOi9puiusOW9leivpuaDheWPr+iHquS4u+e8tOi0uSDmlK/mjIHlvq7kv6Eg5pSv5LuY5a6dIOmTtuihjOaOpeWPo+aUr+S7mCDmlK/mjIHmr4/kuKrlgZzovablnLrmjIflrprkuI3lkIznmoTllYbmiLfov5vooYzmlLbmrL4g5pSv5LuY5ZCO5Ye65Zy65Zyo5YWN6LS55pe26Ze05YaF5Lya6Ieq5Yqo5oqs5p2GIOKRo+aUr+aMgWFwcOS4iuafpeivoumZhOi/keWBnOi9puWcuiDlr7zoiKog5Y+v55So6L2m5L2N5pWwIOWBnOi9puWcuui0ueeUqCDkvJjmg6DliLgg6K+E5YiGIOivhOiuuuetiSDlj6/pooTnuqbovabkvY0g4pGk5pat55S15pat572R5pSv5oyB5bKX5Lqt5Lq65ZGY5L2/55SoYXBw5Y+v5o6l566h56Gs5Lu26L+b6KGM5YGc6L2m6K6w5b2V55qE5b2V5YWlIOaKgOacr+aetuaehCDlkI7nq6/lvIDlj5Hor63oqIBqYXZhIOahhuaetm9hdXRoMiBzcHJpbmcg5oiQ6ZW/6Lev57q/IOS9huWtpuWIsOS4jeS7heS7heaYr0phdmEg5Lia55WM6aaW5Liq5pSv5oyB5riQ6L+b5byP57uE5Lu25YyW5pS56YCg55qEQW5kcm9pZOe7hOS7tuWMluW8gOa6kOahhuaetiDmlK/mjIHot6jov5vnqIvosIPnlKhTcHJpbmdCb290MiDku47lhaXpl6jliLDlrp7miJgg5peo5Zyo5omT6YCg5Zyo57q/5pyA5L2z55qEIEphdmEg5a2m5Lmg56yU6K6wIOWQq+WNmuWuouiusuino+WSjOa6kOeggeWunuS+iyDljIXmi6wgSmF2YSBTRSDlkowgSmF2YSBXZWJKYXZh6K+K5pat5bel5YW35bm06Jaq55m+5LiH5LqS6IGU572R5p625p6E5biI6K++56iL5paH5qGj5Y+K5rqQ56CBIOWFrOW8gOmDqOWIhiBBbmRyb2lkSHR0cENhcHR1cmXnvZHnu5zor4rmlq3lt6Xlhbcg5piv5LiA5qy+QW5kcm9pZOaJi+acuuaKk+WMhei9r+S7tiDkuLvopoHlip/og73ljIXmi6wg5omL5py656uv5oqT5YyFIFBJTkcgRE5TIFRyYWNlUm91dGXor4rmlq0g5oqT5YyFSEFS5pWw5o2u5LiK5Lyg5YiG5LqrIOS9oOS5n+WPr+S7peeci+aIkOaYr0FuZHJvaWTniYjnmoQgRmlkZGxlciBvIOi/meWPr+iDveaYr+WPsuS4iuWKn+iDveacgOWFqOeahEphdmHmnYPpmZDorqTor4HmoYbmnrYg55uu5YmN5bey6ZuG5oiQIOeZu+W9leiupOivgSDmnYPpmZDorqTor4Eg5YiG5biD5byPU2Vzc2lvbuS8muivnSDlvq7mnI3liqHnvZHlhbPpibTmnYMg5Y2V54K555m75b2VIE9BdXRoMiDouKLkurrkuIvnur8gUmVkaXPpm4bmiJAg5YmN5ZCO5Y+w5YiG56a7IOiusOS9j+aIkeaooeW8jyDmqKHmi5/ku5bkurrotKblj7cg5Li05pe26Lqr5Lu95YiH5o2iIOi0puWPt+WwgeemgSDlpJrotKblj7forqTor4HkvZPns7sg5rOo6Kej5byP6Ym05p2DIOi3r+eUseaLpuaIquW8j+mJtOadgyDoirHlvI90b2tlbueUn+aIkCDoh6rliqjnu63nrb4g5ZCM56uv5LqS5pal55m75b2VIOS8muivneayu+eQhiDlr4bnoIHliqDlr4Ygand06ZuG5oiQIFNwcmluZ+mbhuaIkCBXZWJGbHV46ZuG5oiQIEFuZHJvaWTlubPlj7DkuIvnmoTlr4zmlofmnKzop6PmnpDlmagg5pSv5oyBSHRtbOWSjE1hcmtkb3du5pm66IO95Zu+54mH6KOB5Ymq5qGG5p62IOiHquWKqOivhuWIq+i+ueahhiDmiYvliqjosIPoioLpgInljLog5L2/55So6YCP6KeG5Y+Y5o2i6KOB5Ymq5bm255+r5q2j6YCJ5Yy6IOmAgueUqOS6jui6q+S7veivgSDlkI3niYcg5paH5qGj562J54Wn54mH55qE6KOB5YmqIOS/l+WQjSDlj6/lnoLnm7Tot5Eg5Y+v5rC05bmz6LeR55qE6LeR6ams54GvIOWtpuWQjSDlj6/lnoLnm7Tnv7sg5Y+v5rC05bmz57+755qE57+76aG15YWs5ZGKIOWwj+mprOWTpeaKgOacr+WRqOaKpSBBbmRyb2lkIFZpZGVvIFBsYXllciDlronljZPop4bpopHmkq3mlL7lmagg5bCB6KOF5qih5Lu/5oqW6Z+z5bm25a6e546w6aKE5Yqg6L29IOWIl+ihqOaSreaUviDmgqzmta7mkq3mlL4g5bm/5ZGK5pKt5pS+IOW8ueW5lSDph43lraZKYXZh6K6+6K6h5qih5byPIOaYr+S4gOacrOS6kuiBlOe9keecn+WunuahiOS+i+Wunui3teS5puexjSDku6XokL3lnLDop6PlhrPmlrnmoYjkuLrmoLjlv4Mg5LuO5a6e6ZmF5Lia5Yqh5Lit5oq956a75Ye6IOS6pOaYkyDokKXplIAg56eS5p2AIOS4remXtOS7tiDmupDnoIHnrYkyMuS4quecn+WunuWcuuaZryDmnaXlrabkuaDorr7orqHmqKHlvI/nmoTov5DnlKgg5qyi6L+O5YWz5rOo5bCP5YKF5ZOlIOW+ruS/oSBmdXN0YWNrIOWFrOS8l+WPtyBidWdzdGFja+iZq+a0nuagiCDljZrlrqIgYnVnc3RhY2sgY25teWJhdGlz5rqQ56CB5Lit5paH5rOo6YeK5LiA5qy+5byA5rqQ55qER0lG5Zyo57q/5YiG5LqrQXBwIOS5kOi2o+WwseimgeWSjOS4lueVjOWIhuS6qyBNUHVzaOW8gOa6kOWunuaXtua2iOaBr+aOqOmAgeezu+e7n+WcqOe6v+S6keebmCDnvZHnm5ggT25lRHJpdmUg5LqR5a2Y5YKoIOengeacieS6kSDlr7nosaHlrZjlgqggaDVhaeWfuuS6jlNwcmluZyBCb290IDIgeOeahOS4gOermeW8j+WJjeWQjuerr+WIhuemu+W/q+mAn+W8gOWPkeW5s+WPsFhCb290IOW+ruS/oeWwj+eoi+W6jyBVbmlhcHAg5YmN56uvIFZ1ZSBpVmlldyBBZG1pbiDlkI7nq6/liIbluIPlvI/pmZDmtYEg5ZCM5q2l6ZSBIOmqjOivgeeggSBTbm93Rmxha2Xpm6roirHnrpfms5VJRCDliqjmgIHmnYPpmZAg5pWw5o2u5p2D6ZmQIOW3peS9nOa1gSDku6PnoIHnlJ/miJAg5a6a5pe25Lu75YqhIOekvuS6pOi0puWPtyDnn63kv6HnmbvlvZUg5Y2V54K555m75b2VIE9BdXRoMuW8gOaUvuW5s+WPsCDlrqLmnI3mnLrlmajkurog5pWw5o2u5aSn5bGPIOaal+m7keaooeW8j0d1bnPln7rkuo5TcHJpbmdCb290IDIg6Ie05Yqb5LqO5YGa5pu0566A5rSB55qE5ZCO5Y+w566h55CG57O757ufIOWujOe+juaVtOWQiOmhueebruS7o+eggeeugOa0gSDms6jph4rkuLDlr4wg5LiK5omL5a655piTIOWQjOaXtkd1bnPljIXlkKvorrjlpJrln7rnoYDmqKHlnZcg55So5oi3566h55CGIOinkuiJsueuoeeQhiDpg6jpl6jnrqHnkIYg5a2X5YW4566h55CG562JMSDkuKrmqKHlnZcg5Y+v5Lul55u05o6l5L2c5Li65LiA5Liq5ZCO5Y+w566h55CG57O757uf55qE6ISa5omL5p62ICBBbmRyb2lkIOeJiOacrOabtOaWsOS4gOS4queugOa0geiAjOS8mOmbheeahEFuZHJvaWTljp/nlJ9VSeahhuaetiDop6PmlL7kvaDnmoTlj4zmiYsg5LiA5aWX5a6M5pW05pyJ5pWI55qEYW5kcm9pZOe7hOS7tuWMluaWueahiCDmlK/mjIHnu4Tku7bnmoTnu4Tku7blrozlhajpmpTnprsg5Y2V54us6LCD6K+VIOmbhuaIkOiwg+ivlSDnu4Tku7bkuqTkupIgVUnot7Povawg5Yqo5oCB5Yqg6L295Y246L29562J5Yqf6IO96YCC55So5LqOSmF2YeWSjEFuZHJvaWTnmoTlv6vpgJ8g5L2O5YaF5a2Y5Y2g55So55qE5rGJ5a2X6L2s5ou86Z+z5bqTIENvZGVzIG9mIG15IE1PT0MgQ291cnNlIFxcdTAwM2PmiJHlnKjmhZXor77nvZHkuIrnmoTor77nqIsg566X5rOV5LiO5pWw5o2u57uT5p6EIOekuuS+i+S7o+eggSDljIXmi6xDIOWSjEphdmHniYjmnKwg6K++56iL55qE5pu05aSa5pu05paw5YaF5a655Y+K6L6F5Yqp57uD5Lmg5Lmf5bCG6YCQ5q2l5re75Yqg6L+b6L+Z5Liq5Luj56CB5LuTICBIb3BlIEJvb3Qg5LiA5qy+546w5Luj5YyW55qE6ISa5omL5p626aG555uu5LiA5Liq566A5Y2V5ryC5Lqu55qEU1NNIFNwcmluZyBTcHJpbmdNVkMgTXliYXRpcyDljZrlrqLns7vnu5/moLnmja5Hc29u5bqT5L2/55So55qE6KaB5rGCIOWwhkpTT05PYmplY3TmoLzlvI/nmoRTdHJpbmcg6Kej5p6Q5oiQ5a6e5L2TQuermSDlk5Tlk6nlk5Tlk6kgQmlsaWJpbGkg6Ieq5Yqo562+5Yiw5oqV5biB5bel5YW3IOavj+Wkqei9u+advuiOt+WPljY157uP6aqM5YC8IOaUr+aMgeavj+aXpeiHquWKqOaKleW4gSDpk7bnk5zlrZDlhZHmjaLnoazluIEg6aKG5Y+W5aSn5Lya5ZGY56aP5YipIOWkp+S8muWRmOaciOW6lee7meiHquW3seWFheeUteetieWKn+iDvSDlkZAg6LW25b+r5ZKM5oiR5LiA6LW35oiQ5Li6THY25ZCnIElKUGF5IOiuqeaUr+S7mOinpuaJi+WPr+WPiiDlsIHoo4Xkuoblvq7kv6HmlK/ku5ggUVHmlK/ku5gg5pSv5LuY5a6d5pSv5LuYIOS6rOS4nOaUr+S7mCDpk7bogZTmlK/ku5ggUGF5UGFsIOaUr+S7mOetieW4uOeUqOeahOaUr+S7mOaWueW8j+S7peWPiuWQhOenjeW4uOeUqOeahOaOpeWPoyDkuI3kvp3otZbku7vkvZXnrKzkuInmlrkgbXZjIOahhuaetiDku4Xku4XkvZzkuLrlt6Xlhbfkvb/nlKjnroDljZXlv6vpgJ/lrozmiJDmlK/ku5jmqKHlnZfnmoTlvIDlj5Eg5Y+v6L275p2+5bWM5YWl5Yiw5Lu75L2V57O757uf6YeMIOWPs+S4iuinkueCueS4i+Wwj+aYn+aYnyAgSGlnaCBxdWFsaXR5IHB1cmUgV2VleCBkZW1vIOe9keaYk+S4pemAiSBBcHAg5oSf5Y+XIFdlZXgg5byA5Y+RQW5kcm9pZCDlv6vpgJ/lrp7njrDmlrDmiYvlvJXlr7zlsYLnmoTlupMg6YCa6L+H566A5rSB6ZO+5byP6LCD55SoIOS4gOihjOS7o+eggeWunueOsOW8leWvvOWxgueahOaYvuekuumAmui/h+agh+etvuebtOaOpeeUn+aIkHNoYXBlIOaXoOmcgOWGjeWGmXNoYXBlIHhtbCDmnKzlupPmmK/kuIDmrL7ln7rkuo5SeEphdmEyIFJldHJvZml0MuWunueOsOeugOWNleaYk+eUqOeahOe9kee7nOivt+axguahhuaetiDnu5PlkIhhbmRyb2lk5bmz5Y+w54m55oCn55qE572R57uc5bCB6KOF5bqTIOmHh+eUqGFwaemTvuW8j+iwg+eUqOS4gOeCueWIsOW6lSDpm4bmiJBjb29raWXnrqHnkIYg5aSa56eN57yT5a2Y5qih5byPIOaegeeugGh0dHBz6YWN572uIOS4iuS8oOS4i+i9vei/m+W6puaYvuekuiDor7fmsYLplJnor6/oh6rliqjph43or5Ug6K+35rGC5pC65bimdG9rZW4g5pe26Ze05oizIOetvuWQjXNpZ27liqjmgIHphY3nva4g6Ieq5Yqo55m75b2V5oiQ5Yqf5ZCO6K+35rGC6YeN5Y+R5Yqf6IO9IDPnp43lsYLmrKHnmoTlj4LmlbDorr7nva7pu5jorqTlhajlsYDlsYDpg6gg6buY6K6k5qCH5YeGQXBpUmVzdWx05ZCM5pe25Y+v5Lul5pSv5oyB6Ieq5a6a5LmJ55qE5pWw5o2u57uT5p6EIOW3sue7j+iDvea7oei2s+eOsOWcqOeahOWkp+mDqOWIhue9kee7nOivt+axgiBBbmRyb2lkIEJMReiTneeJmemAmuS/oeW6kyDln7rkuo5GbGlua+WunueOsOeahOWVhuWTgeWunuaXtuaOqOiNkOezu+e7nyBmbGlua+e7n+iuoeWVhuWTgeeDreW6piDmlL7lhaVyZWRpc+e8k+WtmCDliIbmnpDml6Xlv5fkv6Hmga8g5bCG55S75YOP5qCH562+5ZKM5a6e5pe26K6w5b2V5pS+5YWlSGJhc2Ug5Zyo55So5oi35Y+R6LW35o6o6I2Q6K+35rGC5ZCOIOagueaNrueUqOaIt+eUu+WDj+mHjeaOkuW6j+eDreW6puamnCDlubbnu5PlkIjljY/lkIzov4fmu6TlkozmoIfnrb7kuKTkuKrmjqjojZDmqKHlnZfkuLrmlrDnlJ/miJDnmoTmppzljZXnmoTmr4/kuIDkuKrkuqflk4Hmt7vliqDlhbPogZTkuqflk4Eg5pyA5ZCO6L+U5Zue5paw55qE55So5oi35YiX6KGoIOaSreaUvuWZqOWfuuehgOW6kyDkuJPms6jkuo7mkq3mlL7op4blm77nu4Tku7bnmoTpq5jlpI3nlKjmgKflkoznu4Tku7bpl7TnmoTkvY7ogKblkIgg6L275p2+5aSE55CG5aSN5p2C5Lia5YqhIOWbvueJh+mAieaLqeW6kyDljZXpgIkg5aSa6YCJIOaLjeeFpyDoo4Hliaog5Y6L57ypIOiHquWumuS5iSDljIXmi6zop4bpopHpgInmi6nlkozlvZXliLYgRGF0YVjpm4bmiJDlj6/op4bljJbpobXpnaIg6YCJ5oup5pWw5o2u5rqQ5Y2z5Y+v5LiA6ZSu55Sf5oiQ5pWw5o2u5ZCM5q2l5Lu75YqhIOaUr+aMgeetieaVsOaNrua6kCDmibnph4/liJvlu7pSREJNU+aVsOaNruWQjOatpeS7u+WKoSDpm4bmiJDlvIDmupDosIPluqbns7vnu58g5pSv5oyB5YiG5biD5byPIOWinumHj+WQjOatpeaVsOaNriDlrp7ml7bmn6XnnIvov5DooYzml6Xlv5cg55uR5o6n5omn6KGM5Zmo6LWE5rqQIEtJTEzov5DooYzov5vnqIsg5pWw5o2u5rqQ5L+h5oGv5Yqg5a+G562JICBEZXByZWNhdGVkIGFuZHJvaWQg6Ieq5a6a5LmJ5pel5Y6G5o6n5Lu2IOaUr+aMgeW3puWPs+aXoOmZkOa7keWKqCDlkajmnIjliIfmjaIg5qCH6K6w5pel5pyf5pi+56S6IOiHquWumuS5ieaYvuekuuaViOaenOi3s+i9rOWIsOaMh+WumuaXpeacn+S4gOS4qumAmui/h+WKqOaAgeWKoOi9veacrOWcsOearuiCpOWMhei/m+ihjOaNouiCpOeahOearuiCpOahhuaetui/meaYr1JlZFNwaWRlcuekvuWMuuaIkOWRmOWOn+WIm+S4jue7tOaKpOeahEphdmHlpJrnur/nqIvns7vliJfmlofnq6Ag5LiA56uZ5byPQXBhY2hlIEthZmth6ZuG576k5oyH5qCH55uR5o6n5LiO6L+Q57u0566h5o6n5bmz5Y+w5b+r6YCf5byA5Y+R5bel5YW357G75pS26ZuGIOWPsuS4iuacgOWFqOeahOW8gOWPkeW3peWFt+exuyDmrKLov45Gb2xsb3cgRm9yayBTdGFy5ZCO56uv5oqA5pyv5oC757uTIOWMheaLrEphdmHln7rnoYAgSlZNIOaVsOaNruW6kyBteXNxbCByZWRpcyDorqHnrpfmnLrnvZHnu5wg566X5rOVIOaVsOaNrue7k+aehCDmk43kvZzns7vnu58g6K6+6K6h5qih5byPIOezu+e7n+iuvuiuoSDmoYbmnrbljp/nkIYg5pyA5L2z6ZiF6K+75Zyw5Z2AQW5kcm9pZOa6kOeggeiuvuiuoeaooeW8j+WIhuaekOmhueebruWPr+iDveaYr+acgOWlveeahOaUr+S7mFNESyDlgZzmraLnu7TmiqQg57uE5Lu25YyW57u85ZCI5qGI5L6LIOWMheWQq+W+ruS/oeaWsOmXuyDlpLTmnaHop4bpopEg576O5aWz5Zu+54mHIOeZvuW6pumfs+S5kCDlubLmtLvpm4bkuK3okKUg546pQW5kcm9pZCDosYbnk6Por7vkuabnlLXlvbEg55+l5LmO5pel5oql562J562J5qih5Z2XIOaetuaehOaooeW8jyDnu4Tku7bljJbpmL/ph4xWTGF5b3V0IOiFvuiur1g1IOiFvuiur2J1Z2x5IOiejeWQiOW8gOWPkeS4remcgOimgeeahOWQhOenjeWwj+ahiOS+iyDlvIDmupBPQeezu+e7nyDnoIHkupFHVlAgSmF2YeW8gOa6kG9hIOS8geS4mk9B5Yqe5YWs5bmz5Y+wIOS8geS4mk9BIOWNj+WQjOWKnuWFrE9BIOa1geeoi+W5s+WPsE9BIE8yT0EgT0Eg5pSv5oyB5Zu95Lqn6bqS6bqf5pON5L2c57O757uf5ZKM5Zu95Lqn5pWw5o2u5bqTIOi+vuaipiDkurrlpKfph5Hku5Mg5pS/5YqhT0Eg5Yab5bel5L+h5oGv5YyWT0Hku6VTcHJpbmcgQ2xvdWQgTmV0ZmxpeOS9nOS4uuacjeWKoeayu+eQhuWfuuehgCDlsZXnpLrln7rkuo50Y2PmgJ3mg7PmiYDlrp7njrDnmoTliIbluIPlvI/kuovliqHop6PlhrPmlrnmoYjkuIDkuKrluK7liqnmgqjlrozmiJDku47nvKnnlaXop4blm77liLDljp/op4blm77ml6DnvJ3ov4fmuKHovazlj5jnmoTnpZ7lpYfmoYbmnrYg57O757uf6YeN5p6E5LiO6L+B56e75oyH5Y2XIOaJi+aKiuaJi+aVmeS9oOWIhuaekCDor4TkvLDnjrDmnInns7vnu58g5Yi25a6a6YeN5p6E562W55WlIOaOoue0ouWPr+ihjOmHjeaehOaWueahiCDmkK3lu7rmtYvor5XpmLLmiqTnvZEg6L+b6KGM57O757uf5p625p6E6YeN5p6EIOacjeWKoeaetuaehOmHjeaehCDmqKHlnZfph43mnoQg5Luj56CB6YeN5p6EIOaVsOaNruW6k+mHjeaehCDph43mnoTlkI7nmoTmnrbmnoTlrojmiqTniYjmnKzmo4DmtYvljYfnuqcg5pu05pawIOW6k+Wwj+ivtOeyvuWTgeWxi+aYr+S4gOS4quWkmuW5s+WPsCB3ZWIg5a6J5Y2TYXBwIOW+ruS/oeWwj+eoi+W6jyDlip/og73lrozlloTnmoTlsY/luZXoh6rpgILlupTlsI/or7TmvKvnlLvov57ovb3ns7vnu58g5YyF5ZCr57K+5ZOB5bCP6K+05LiT5Yy6IOi9u+Wwj+ivtOS4k+WMuuWSjOa8q+eUu+S4k+WMuiDljIXmi6zlsI/or7Qg5ryr55S75YiG57G7IOWwj+ivtCDmvKvnlLvmkJzntKIg5bCP6K+0IOa8q+eUu+aOkuihjCDlrozmnKzlsI/or7Qg5ryr55S7IOWwj+ivtCDmvKvnlLvor4TliIYg5bCP6K+0IOa8q+eUu+WcqOe6v+mYheivuyDlsI/or7Qg5ryr55S75Lmm5p62IOWwj+ivtCDmvKvnlLvpmIXor7vorrDlvZUg5bCP6K+05LiL6L29IOWwj+ivtOW8ueW5lSDlsI/or7Qg5ryr55S76Ieq5Yqo6YeH6ZuGIOabtOaWsCDnuqDplJkg5bCP6K+05YaF5a656Ieq5Yqo5YiG5Lqr5Yiw5b6u5Y2aIOmCruS7tuiHquWKqOaOqOW5vyDpk77mjqXoh6rliqjmjqjpgIHliLDnmb7luqbmkJzntKLlvJXmk47nrYnlip/og70gQW5kcm9pZCDlvr3nq6Dmjqfku7Yg6Ie05Yqb5LqO5omT6YCg5LiA5qy+5p6B6Ie05L2T6aqM55qEIHd3dyB3YW5hbmRyb2lkIGNvbSDlrqLmiLfnq68g55+l6K+G5ZKM576O5piv5Y+v5Lul5bm25a2Y55qE5ZOmUUFRbiDiiacg4ommIG4g5LuO5rqQ56CB5bGC6Z2iIOWJluaekOaMluaOmOS6kuiBlOe9keihjOS4muS4u+a1geaKgOacr+eahOW6leWxguWunueOsOWOn+eQhiDkuLrlub/lpKflvIDlj5HogIUg4oCc5o+Q5Y2H5oqA5pyv5rex5bqm4oCdIOaPkOS+m+S+v+WIqSDnm67liY3lvIDmlL4gU3ByaW5nIOWFqOWutuahtiBNeWJhdGlzIE5ldHR5IER1YmJvIOahhuaetiDlj4ogUmVkaXMgVG9tY2F0IOS4remXtOS7tuetiVJlZGlzIOS4gOermeW8j+euoeeQhuW5s+WPsCDmlK/mjIHpm4bnvqTnmoTnm5Hmjqcg5a6J6KOFIOeuoeeQhiDlkYrorabku6Xlj4rln7rmnKznmoTmlbDmja7mk43kvZzor6Xpobnnm67kuI3lho3nu7TmiqQg5LuF5L6b5a2m5Lmg5Y+C6ICD5LiT5rOo5om56YeP5o6o6YCB55qE5bCP6ICM576O55qE5bel5YW3IOebruWJjeaUr+aMgSDmqKHmnb/mtojmga8g5YWs5LyX5Y+3IOaooeadv+a2iOaBryDlsI/nqIvluo8g5b6u5L+h5a6i5pyN5raI5oGvIOW+ruS/oeS8geS4muWPtyDkvIHkuJrlvq7kv6Hmtojmga8g6Zi/6YeM5LqR55+t5L+hIOmYv+mHjOWkp+S6juaooeadv+efreS/oSDohb7orq/kupHnn63kv6Eg5LqR54mH572R55+t5L+hIEUgTWFpbCBIVFRQ6K+35rGCIOmSiemSiSDljY7kuLrkupHnn63kv6Eg55m+5bqm5LqR55+t5L+hIOWPiOaLjeS6keefreS/oSDkuIPniZvkupHnn63kv6FBbmRyb2lkIOW5s+WPsOW8gOa6kOWkqeawlCBBcHAg6YeH55So562J5byA5rqQ5bqT5p2l5a6e546wIFNwcmluZ0Jvb3Qg55u45YWz5ryP5rSe5a2m5Lmg6LWE5paZIOWIqeeUqOaWueazleWSjOaKgOW3p+WQiOmbhiDpu5Hnm5Llronlhajor4TkvLAgY2hlY2sgbGlzdEFuZHJvaWQg5p2D6ZmQ6K+35rGC5qGG5p62IOW3sumAgumFjSBBbmRyb2lkIDEx5b6u5L+hU0RLIEpBVkEg5YWs5LyX5bmz5Y+wIOW8gOaUvuW5s+WPsCDllYbmiLflubPlj7Ag5pyN5Yqh5ZWG5bmz5Y+wICBRTVHmmK/ljrvlk6rlhL/nvZHlhoXpg6jlub/ms5vkvb/nlKjnmoTmtojmga/kuK3pl7Tku7Yg6IeqMiAxMuW5tOivnueUn+S7peadpeWcqOWOu+WTquWEv+e9keaJgOacieS4muWKoeWcuuaZr+S4reW5v+azm+eahOW6lOeUqCDljIXmi6zot5/kuqTmmJPmga/mga/nm7jlhbPnmoTorqLljZXlnLrmma8g5Lmf5YyF5ous5oql5Lu35pCc57Si562J6auY5ZCe5ZCQ6YeP5Zy65pmvICBKYXZhIDIz56eN6K6+6K6h5qih5byP5YWo5b2S57qzbGludXjov5Dnu7Tnm5Hmjqflt6Xlhbcg5pSv5oyB57O757uf5L+h5oGvIOWGheWtmCBjcHUg5rip5bqmIOejgeebmOepuumXtOWPiklPIOehrOebmHNtYXJ0IOezu+e7n+i0n+i9vSDnvZHnu5zmtYHph48g6L+b56iL562J55uR5o6nIEFQSeaOpeWPoyDlpKflsY/lsZXnpLog5ouT5omR5Zu+IOerr+WPo+ebkeaOpyBkb2NrZXLnm5Hmjqcg5pel5b+X5paH5Lu255uR5o6nIOaVsOaNruWPr+inhuWMliB3ZWJTU0jlt6Xlhbcg5aCh5Z6S5py6IOi3s+adv+acuiDov5nlj6/og73mmK/lhajnvZHmnIDlpb3nlKjnmoRWaWV3UGFnZXLova7mkq3lm74g566A5Y2VIOmrmOaViCDkuIDooYzku6PnoIHlrp7njrDlvqrnjq/ova7mkq0g5LiA5bGP5LiJ6aG15Lu75oSP5Y+YIOaMh+ekuuWZqOagt+W8j+S7u+S9oOaMkSDkuIDnp43nroDljZXmnInmlYjnmoRhbmRyb2lk57uE5Lu25YyW5pa55qGIIOaUr+aMgee7hOS7tueahOS7o+eggei1hOa6kOmalOemuyDljZXni6zosIPor5Ug6ZuG5oiQ6LCD6K+VIOe7hOS7tuS6pOS6kiBVSei3s+i9rCDnlJ/lkb3lkajmnJ/nrYnlrozmlbTlip/og70g5LiA5Liq5by65aSnIDEgJSDlhbzlrrkg5pSv5oyBIEFuZHJvaWRYIOaUr+aMgSBLb3RsaW7lubbkuJTngbXmtLvnmoTnu4Tku7bljJbmoYbmnrZKUHJlc3Mg5LiA5Liq5L2/55SoIEphdmEg5byA5Y+R55qE5bu656uZ56We5ZmoIOebruWJjeW3sue7j+aciSAxIHcg572R56uZ5L2/55SoIEpQcmVzcyDov5vooYzpqbHliqgg5YW25Lit5YyF5ous5aSa5Liq5pS/5bqc5py65p6EIDIg5LiK5biC5YWs5Y+4IOS4reenkemZoiDnuqIg5a2X5Lya562JIOWIhuW4g+W8j+S6i+WKoeaYk+eUqOeahOi9u+mHj+WMlue9kee7nOeIrOiZqyBBbmRyb2lk57O757uf5rqQ56CB5YiG5p6Q6YeN5p6E5Lit5LiA5qy+5YWN6LS555qE5pWw5o2u5Y+v6KeG5YyW5bel5YW3IOaKpeihqOS4juWkp+Wxj+iuvuiuoSDnsbvkvLzkuo5leGNlbOaTjeS9nOmjjuagvCDlnKjnur/mi5bmi73lrozmiJDmiqXooajorr7orqEg5Yqf6IO95ra155uWIOaKpeihqOiuvuiuoSDlm77lvaLmiqXooagg5omT5Y2w6K6+6K6hIOWkp+Wxj+iuvuiuoeetiSDmsLjkuYXlhY3otLkg56eJ5om/4oCc566A5Y2VIOaYk+eUqCDkuJPkuJrigJ3nmoTkuqflk4HnkIblv7Ug5p6B5aSn55qE6ZmN5L2O5oql6KGo5byA5Y+R6Zq+5bqmIOe8qeefreW8gOWPkeWRqOacnyDoioLnnIHmiJDmnKwg6Kej5Yaz5ZCE57G75oql6KGo6Zq+6aKYIEFuZHJvaWQgQWN0aXZpdHkg5ruR5Yqo6L+U5ZueIOaUr+aMgeW+ruS/oea7keWKqOi/lOWbnuagt+W8jyDmqKrlsY/mu5Hliqjov5Tlm54g5YWo5bGP5ruR5Yqo6L+U5ZueU3ByaW5nQm9vdCDln7rnoYDmlZnnqIsg5LuO5YWl6Zeo5Yiw5LiK55i+IOWfuuS6jjIgTTXliLbkvZwg5Lu/5b6u5L+h6KeG6aKR5ouN5pGEVUkg5Z+65LqOZmZtcGVn55qE6KeG6aKR5b2V5Yi257yW6L6RUHl0aG9uIDEg5aSp5LuO5paw5omL5Yiw5aSn5biIIOWIhuS6qyBHaXRIdWIg5LiK5pyJ6LajIOWFpemXqOe6p+eahOW8gOa6kOmhueebruS4reiLseaWh+aVj+aEn+ivjSDor63oqIDmo4DmtYsg5Lit5aSW5omL5py6IOeUteivneW9kuWxnuWcsCDov5DokKXllYbmn6Xor6Ig5ZCN5a2X5o6o5pat5oCn5YirIOaJi+acuuWPt+aKveWPliDouqvku73or4Hmir3lj5Yg6YKu566x5oq95Y+WIOS4reaXpeaWh+S6uuWQjeW6kyDkuK3mlofnvKnlhpnlupMg5ouG5a2X6K+N5YW4IOivjeaxh+aDheaEn+WAvCDlgZznlKjor40g5Y+N5Yqo6K+N6KGoIOaatOaBkOivjeihqCDnuYHnroDkvZPovazmjaIg6Iux5paH5qih5ouf5Lit5paH5Y+R6Z+zIOaxquWzsOatjOivjeeUn+aIkOWZqCDogYzkuJrlkI3np7Dor43lupMg5ZCM5LmJ6K+N5bqTIOWPjeS5ieivjeW6kyDlkKblrpror43lupMg5rG96L2m5ZOB54mM6K+N5bqTIOaxvei9pumbtuS7tuivjeW6kyDov57nu63oi7HmlofliIflibIg5ZCE56eN5Lit5paH6K+N5ZCR6YePIOWFrOWPuOWQjeWtl+Wkp+WFqCDlj6Tor5for43lupMgSVTor43lupMg6LSi57uP6K+N5bqTIOaIkOivreivjeW6kyDlnLDlkI3or43lupMg5Y6G5Y+y5ZCN5Lq66K+N5bqTIOivl+ivjeivjeW6kyDljLvlrabor43lupMg6aWu6aOf6K+N5bqTIOazleW+i+ivjeW6kyDmsb3ovabor43lupMg5Yqo54mp6K+N5bqTIOS4reaWh+iBiuWkqeivreaWmSDkuK3mlofosKPoqIDmlbDmja4g55m+5bqm5Lit5paH6Zeu562U5pWw5o2u6ZuGIOWPpeWtkOebuOS8vOW6puWMuemFjeeul+azlembhuWQiCBiZXJ06LWE5rqQIOaWh+acrOeUn+aIkCDmkZjopoHnm7jlhbPlt6XlhbcgY29jb05MUOS/oeaBr+aKveWPliAyIDIx5bm05pyA5paw5oC757uTIOmYv+mHjCDohb7orq8g55m+5bqmIOe+juWboiDlpLTmnaHnrYnmioDmnK/pnaLor5Xpopjnm64g5Lul5Y+K562U5qGIIOS4k+WutuWHuumimOS6uuWIhuaekOaxh+aAuyBBaUxlYXJuaW5nIOacuuWZqOWtpuS5oCBNYWNoaW5lTGVhcm5pbmcgTUwg5rex5bqm5a2m5LmgIERlZXBMZWFybmluZyBETCDoh6rnhLbor63oqIDlpITnkIYgTkxQMTIzIDbmmbrog73liLfnpagg6K6i56Wo57uT5be05Lit5paH5YiG6K+NIOWKqOaJi+Wtpua3seW6puWtpuS5oCDpnaLlkJHkuK3mlofor7vogIUg6IO96L+Q6KGMIOWPr+iuqOiuuiDkuK3oi7HmlofniYjooqvlhajnkIMxNzXmiYDlpKflrabph4fnlKjmlZnlraYg5Lit5paH5YiG6K+NIOivjeaAp+agh+azqCDlkb3lkI3lrp7kvZPor4bliKsg5L6d5a2Y5Y+l5rOV5YiG5p6QIOivreS5ieS+neWtmOWIhuaekCDmlrDor43lj5HnjrAg5YWz6ZSu6K+N55+t6K+t5o+Q5Y+WIOiHquWKqOaRmOimgSDmlofmnKzliIbnsbvogZrnsbsg5ou86Z+z566A57mB6L2s5o2iIOiHqueEtuivreiogOWkhOeQhuW+ruS/oeS4quS6uuWPt+aOpeWPoyDlvq7kv6HmnLrlmajkurrlj4rlkb3ku6TooYzlvq7kv6Eg5LiJ5Y2B6KGM5Y2z5Y+v6Ieq5a6a5LmJ5Liq5Lq65Y+35py65Zmo5Lq6IOaVsOaNrue7k+aehOWSjOeul+azleW/heefpeW/heS8mueahDUg5Liq5Luj56CB5a6e546wSnVtcFNlcnZlciDmmK/lhajnkIPpppbmrL7lvIDmupDnmoTloKHlnpLmnLog5piv56ym5ZCIIDRBIOeahOS4k+S4mui/kOe7tOWuieWFqOWuoeiuoeezu+e7nyDpo57moagg5qC45b+D5qGG5p62IOa3seW6puWtpuS5oCDmnLrlmajlrabkuaDpq5jmgKfog73ljZXmnLog5YiG5biD5byP6K6t57uD5ZKM6Leo5bmz5Y+w6YOo572yIOS4reWbveeoi+W6j+WRmOWuueaYk+WPkemfs+mUmeivr+eahOWNleivjeW+ruS/oSDot7PkuIDot7MgUHl0aG9uIOi+heWKqSBweXRob27mqKHmi5/nmbvpmYbkuIDkupvlpKflnovnvZHnq5kg6L+Y5pyJ5LiA5Lqb566A5Y2V55qE54is6JmrIOW4jOacm+WvueS9oOS7rOacieaJgOW4ruWKqSDvuI8g5aaC5p6c5Zac5qyi6K6w5b6X57uZ5Liqc3RhcuWTpiAg572R57uc54is6Jmr5a6e5oiYIOa3mOWunSDkuqzkuJwg572R5piT5LqRIELnq5kgMTIzIDYg5oqW6Z+zIOeslOi2o+mYgSDmvKvnlLvlsI/or7TkuIvovb0g6Z+z5LmQ55S15b2x5LiL6L29562JUHl0aG9u54is6Jmr5Luj55CGSVDmsaAgcHJveHkgcG9vbCB3dGZweXRob27nmoTkuK3mlofnv7vor5Eg5pa95bel57uT5p2fIOiDveWKm+aciemZkCDmrKLov47luK7miJHmlLnov5vnv7vor5Hmj5DkvpvlpJrmrL4gU2hhZG93cm9ja2V0IOinhOWImSDluKblub/lkYrov4fmu6Tlip/og70g55So5LqOIGlPUyDmnKrotorni7Horr7lpIfpgInmi6nmgKflnLDoh6rliqjnv7vlopkgIDEyMyA2IOi0reelqOWKqeaJiyDmlK/mjIHpm4bnvqQg5aSa6LSm5Y+3IOWkmuS7u+WKoei0reelqOS7peWPiiBXZWIg6aG16Z2i566h55CGIHdhbGxlIOeTpuWKmyBEZXZvcHPlvIDmupDpobnnm67ku6PnoIHpg6jnvbLlubPlj7DkuIDkupvpnZ7luLjmnInotqPnmoRweXRob27niKzomavkvovlrZAg5a+55paw5omL5q+U6L6D5Y+L5aW9IOS4u+imgeeIrOWPlua3mOWunSDlpKnnjKsg5b6u5L+hIOixhueToyBRUeetiee9keermeacuuWZqOWtpuS5oOebuOWFs+aVmeeoizEgQ2hpbmVzZSBXb3JkIFZlY3RvcnMg5LiK55m+56eN6aKE6K6t57uD5Lit5paH6K+N5ZCR6YePIOe9keaYk+S6kemfs+S5kOWRveS7pOihjOeJiOacrOS4gOasvuWFpemXqOe6p+eahOS6uuiEuCDop4bpopEg5paH5a2X5qOA5rWL5Lul5Y+K6K+G5Yir55qE6aG555uuICDnvJbnqIvpmo/mg7Mg5pW055CG55qEIOWkquWtkOWFmuWFs+ezu+e9kee7nCDkuJPpl6jmj63pnLLotbXlm73nmoTmnYPotLXlvq7kv6HliqnmiYsgMSDmr4/ml6Xlrprml7bnu5nlpb3lj4sg5aWz5Y+LIOWPkemAgeWumuWItua2iOaBryAyIOacuuWZqOS6uuiHquWKqOWbnuWkjeWlveWPiyAzIOe+pOWKqeaJi+WKn+iDvSDkvovlpoIg5p+l6K+i5Z6D5Zy+5YiG57G7IOWkqeawlCDml6XljoYg55S15b2x5a6e5pe256Wo5oi/IOW/q+mAkueJqea1gSBQTTIgNeetiSDkuoznu7TnoIHnlJ/miJDlmagg5pSv5oyBIGdpZiDliqjmgIHlm77niYfkuoznu7TnoIEg6Zi/5biD6YeP5YyW5Lqk5piT57O757ufIOiCoeelqCDmnJ/mnYMg5pyf6LSnIOavlOeJueW4gSDmnLrlmajlrabkuaAg5Z+65LqOcHl0aG9u55qE5byA5rqQ6YeP5YyW5Lqk5piTIOmHj+WMluaKlei1hOaetuaehCBib29rIOS4reWNjuaWsOWNjuWtl+WFuOaVsOaNruW6kyDljIXmi6zmrYflkI7or60g5oiQ6K+tIOivjeivrSDmsYnlrZcgIEdpdCBBV1MgR29vZ2xlIOmVnOWDjyBTUyBTU1IgVk1FU1PoioLngrnooYzkuJrnoJTnqbbmiqXlkYrnmoTnn6Xor4blgqjlpIflupPkuK3mlofnv7vor5HmiYvlhpnlrp7njrDmnY7oiKog57uf6K6h5a2m5Lmg5pa55rOVIOS5puS4reWFqOmDqOeul+azlSBQeXRob24g5oqW6Z+z5py65Zmo5Lq6IOiuuuWmguS9leWcqOaKlumfs+S4iuaJvuWIsOa8guS6ruWwj+WnkOWnkO+8nyDov4Hnp7vlrabkuaBweXRob27niKzomavmlZnnqIvns7vliJcg5LuOIOWIsDHlrabkuaBweXRob27niKzomasg5YyF5ous5rWP6KeI5Zmo5oqT5YyFIOaJi+acukFQUOaKk+WMhSDlpoIgZmlkZGxlciBtaXRtcHJveHkg5ZCE56eN54is6Jmr5raJ5Y+K55qE5qih5Z2X55qE5L2/55SoIOWmguetiSDku6Xlj4pJUOS7o+eQhiDpqozor4HnoIHor4bliKsgTXlzcWwgTW9uZ29EQuaVsOaNruW6k+eahHB5dGhvbuS9v+eUqCDlpJrnur/nqIvlpJrov5vnqIvniKzomavnmoTkvb/nlKggY3NzIOeIrOiZq+WKoOWvhumAhuWQkeegtOinoyBKU+eIrOiZq+mAhuWQkSDliIbluIPlvI/niKzomasg54is6Jmr6aG555uu5a6e5oiY5a6e5L6L562JUHl0aG9u6ISa5pysIOaooeaLn+eZu+W9leefpeS5jiDniKzomasg5pON5L2cZXhjZWwg5b6u5L+h5YWs5LyX5Y+3IOi/nOeoi+W8gOacuui2iuadpei2iuWkmueahOe9keermeWFt+acieWPjeeIrOiZq+eJueaApyDmnInnmoTnlKjlm77niYfpmpDol4/lhbPplK7mlbDmja4g5pyJ55qE5L2/55So5Y+N5Lq657G755qE6aqM6K+B56CBIOW7uueri+WPjeWPjeeIrOiZq+eahOS7o+eggeS7k+W6kyDpgJrov4fkuI7kuI3lkIznibnmgKfnmoTnvZHnq5nlgZrmlpfkuokg5peg5oG25oSPIOaPkOmrmOaKgOacryDmrKLov47mj5DkuqTpmr7ku6Xph4fpm4bnmoTnvZHnq5kg5Zug5bel5L2c5Y6f5ZugIOmhueebruaaguWBnCAg5Lq65Lq65b2x6KeGYm90IOWujOWFqOWvueaOpeS6uuS6uuW9seinhuWFqOmDqOaXoOWIoOWHj+i1hOa6kOiOq+eDplB5dGhvbiDkuK3mlodBSeaVmeWtpumjnuahqCDlrpjmlrnmqKHlnovlupMg5YyF5ZCr5aSa56eN5a2m5pyv5YmN5rK/5ZKM5bel5Lia5Zy65pmv6aqM6K+B55qE5rex5bqm5a2m5Lmg5qih5Z6LIOi9u+mHj+e6p+S6uuiEuOajgOa1i+aooeWeiyDnmb7luqbkupEg55m+5bqm572R55uYUHl0aG9u5a6i5oi356uvIFB5dGhvbui/m+mYtiBJbnRlcm1lZGlhdGUgUHl0aG9uIOS4reaWh+eJiCDmj5DkvpvlkIzoirHpobrlrqLmiLfnq68g5Zu96YeRIOWNjuazsOWuouaIt+erryDpm6rnkIPnmoTln7rph5Eg6IKh56Wo6Ieq5Yqo56iL5bqP5YyW5Lqk5piT5Lul5Y+K6Ieq5Yqo5omT5pawIOaUr+aMgei3n+i4qiBqb2lucXVhbnQgcmljZXF1YW50IOaooeaLn+S6pOaYkyDlkowg5a6e55uY6Zuq55CD57uE5ZCIIOmHj+WMluS6pOaYk+e7hOS7tlFVQU5UQVhJUyDmlK/mjIHku7vliqHosIPluqYg5YiG5biD5byP6YOo572y55qEIOiCoeelqCDmnJ/otKcg5pyf5p2DIOa4r+iCoSDomZrmi5/otKfluIEg5pWw5o2uIOWbnua1iyDmqKHmi58g5Lqk5piTIOWPr+inhuWMliDlpJrotKbmiLcg57qv5pys5Zyw6YeP5YyW6Kej5Yaz5pa55qGISU5GTyBTUElERVIg5piv5LiA5Liq6ZuG5LyX5aSa5pWw5o2u5rqQ5LqO5LiA6Lqr55qE54is6Jmr5bel5YW3566xIOaXqOWcqOWuieWFqOW/q+aNt+eahOW4ruWKqeeUqOaIt+aLv+WbnuiHquW3seeahOaVsOaNriDlt6Xlhbfku6PnoIHlvIDmupAg5rWB56iL6YCP5piOIOaUr+aMgeaVsOaNrua6kOWMheaLrEdpdEh1YiBRUemCrueusSDnvZHmmJPpgq7nrrEg6Zi/6YeM6YKu566xIOaWsOa1qumCrueusSBIb3RtYWls6YKu566xIE91dGxvb2vpgq7nrrEg5Lqs5LicIOa3mOWunSDmlK/ku5jlrp0g5Lit5Zu956e75YqoIOS4reWbveiBlOmAmiDkuK3lm73nlLXkv6Eg55+l5LmOIOWTlOWTqeWTlOWTqSDnvZHmmJPkupHpn7PkuZAgUVHlpb3lj4sgUVHnvqQg55Sf5oiQ5pyL5Y+L5ZyI55u45YaMIOa1j+iniOWZqOa1j+iniOWOhuWPsiAxMjMgNiDljZrlrqLlm60gQ1NETuWNmuWuoiDlvIDmupDkuK3lm73ljZrlrqIg566A5LmmIOS4reaWh0JFUlQgd3dt57O75YiX5qih5Z6LIFB5dGhvbuWFpemXqOe9kee7nOeIrOiZq+S5i+eyvuWNjueJiOS4reaWhyBpT1MgTWFjIOW8gOWPkeWNmuWuouWIl+ihqFB5dGhvbue9kemhteW+ruS/oUFQSXBrdXNlZ+WkmumihuWfn+S4reaWh+WIhuivjeW3peWFt+iHquW3seWKqOaJi+WBmuiBiuWkqeacuuWZqOS6uuaVmeeoi+WfuuS6juaQnOeLl+W+ruS/oeaQnOe0oueahOW+ruS/oeWFrOS8l+WPt+eIrOiZq+aOpeWPo+eUqOa3seW6puWtpuS5oOWvueWvueiBlCB2MnJheSB4cmF55aSa55So5oi3566h55CG6YOo572y56iL5bqP5ZCE56eN6ISa5pysIOWFs+S6jiDomb7nsbMgeGlhbWkgY29tIOeZvuW6pue9keebmCBwYW4gYmFpZHUgY29tIDExNee9keebmCAxMTUgY29tIOe9keaYk+mfs+S5kCBtdXNpYyAxNjMgY29tIOeZvuW6pumfs+S5kCBtdXNpYyBiYWlkdSBjb20gMzYg572R55uYIOS6keebmCB5dW5wYW4gY24g6KeG6aKR6Kej5p6QIGZsdnh6IGNvbSBidCB0b3JyZW50IOKGlCBtYWduZXQgZWQyayDmkJzntKIgdHVtYmxyIOWbvueJh+S4i+i9vSB1bnppcOafpeeci+iiq+WIoOeahOW+ruS/oeWlveWPi+WumuaKleaUueWPmOWRvei/kCDorqnml7bpl7TpmarkvaDmhaLmhaLlj5jlr4wgb25yZWd1bGFyaW52ZXN0aW5nIGNvbSDmnLrlmajlrabkuaDlrp7miJggUHl0aG9uMyBrTk4g5Yaz562W5qCRIOi0neWPtuaWryDpgLvovpHlm57lvZIgU1ZNIOe6v+aAp+WbnuW9kiDmoJHlm57lvZJTdGF0aXN0aWNhbCBsZWFybmluZyBtZXRob2RzIOe7n+iuoeWtpuS5oOaWueazlSDnrKwy54mIIOadjuiIqiDnrJTorrAg5Luj56CBIG5vdGVib29rIOWPguiAg+aWh+eMriBFcnJhdGEgbGloYW5nIHN0b2NrIOiCoeelqOezu+e7nyDkvb/nlKhweXRob27ov5vooYzlvIDlj5Eg5Z+65LqO5rex5bqm5a2m5Lmg55qE5Lit5paH6K+t6Z+z6K+G5Yir57O757uf5Lqs5Lic5oqi6LSt5Yqp5omLIOWMheWQq+eZu+W9lSDmn6Xor6LllYblk4HlupPlrZgg5Lu35qC8IOa3u+WKoCDmuIXnqbrotK3nianovaYg5oqi6LSt5ZWG5ZOBIOS4i+WNlSDmn6Xor6LorqLljZXnrYnlip/og73ojqvng6ZQeXRob24g5Lit5paHQUnmlZnlrabmnLrlmajlrabkuaDnrpfms5VweXRob27lrp7njrDmlrDmtarlvq7ljZrniKzomasg55SocHl0aG9u54is5Y+W5paw5rWq5b6u5Y2a5pWw5o2u55qE566X5rOV5Lul5Y+K6YCa55So55Sf5oiQ5a+55oqX572R57uc5Zu+5YOP55Sf5oiQ55qE55CG6K665LiO5a6e6Le156CU56m2IOmdkuWym+Wkp+WtpuW8gOa6kCBPbmxpbmUgSnVkZ2UgUVHnvqQgNDk2NzEgMTI1IGFkbWluIHFkdW9qIGNvbVdlUm9Cb3Qg5piv5LiA5Liq5b6u5L+h5YWs5LyX5Y+35byA5Y+R5qGG5p62IOWfuuS6jkRqYW5nb+eahOWNmuWuouezu+e7nyDkuK3mlofov5HkuYnor40g6IGK5aSp5py65Zmo5Lq6IOaZuuiDvemXruetlOW3peWFt+WMheW8gOa6kOi0oue7j+aVsOaNruaOpeWPo+W6k+W3oemjjuaYr+S4gOasvumAgueUqOS6juS8geS4muWGhee9keeahOa8j+a0nuW/q+mAn+W6lOaApSDlt6HoiKrmiavmj4/ns7vnu58gIOeVquWPt+Wkp+WFqCDop6PlhrPnlLXohJEg5omL5py655yL55S16KeG55u05pKt55qE6Ium5oG8IOaUtumbhuWQhOenjeebtOaSrea6kCDnlLXop4bnm7Tmkq3nvZHnq5nnn6Xor4blm77osLHmnoTlu7og6Ieq5Yqo6Zeu562UIOWfuuS6jmtn55qE6Ieq5Yqo6Zeu562UIOS7peeWvueXheS4uuS4reW/g+eahOS4gOWumuinhOaooeWMu+iNr+mihuWfn+efpeivhuWbvuiwsSDlubbku6Xor6Xnn6Xor4blm77osLHlrozmiJDoh6rliqjpl67nrZTkuI7liIbmnpDmnI3liqEg5Ye65aSE5pys5Zyw55S15b2x5Yiu5YmK5LiO5pW055CG5LiA5L2T5YyW6Kej5Yaz5pa55qGI6Ieq5Yqo5YyW6L+Q57u05bmz5Y+wIENNREIgQ0QgRGV2T3BzIOi1hOS6p+euoeeQhiDku7vliqHnvJbmjpIg5oyB57ut5Lqk5LuYIOezu+e7n+ebkeaOpyDov5Dnu7TnrqHnkIYg6YWN572u566h55CGIHd1a29uZyByb2JvdCDmmK/kuIDkuKrnroDljZUg54G15rS7IOS8mOmbheeahOS4reaWh+ivremfs+WvueivneacuuWZqOS6uiDmmbrog73pn7PnrrHpobnnm64g6L+Y5Y+v6IO95piv6aaW5Liq5pSv5oyB6ISR5py65Lqk5LqS55qE5byA5rqQ5pm66IO96Z+z566x6aG555uuIOiOt+WPluaWl+mxvCDomY7niZkg5ZOU5ZOp5ZOU5ZOpIOaKlumfsyDlv6vmiYvnrYkgNTUg5Liq55u05pKt5bmz5Y+w55qE55yf5a6e5rWB5aqS5L2T5Zyw5Z2AIOebtOaSrea6kCDlkozlvLnluZUg55u05pKt5rqQ5Y+v5ZyoIFBvdFBsYXllciBmbHYganMg562J5pKt5pS+5Zmo5Lit5pKt5pS+IOWuneWhlExpbnV46Z2i5p2/IOeugOWNleWlveeUqOeahOacjeWKoeWZqOi/kOe7tOmdouadv+WGnOS4muefpeivhuWbvuiwsSBBZ3JpS0cg5Yac5Lia6aKG5Z+f55qE5L+h5oGv5qOA57SiIOWRveWQjeWunuS9k+ivhuWIqyDlhbPns7vmir3lj5Yg5pm66IO96Zeu562UIOi+heWKqeWGs+etlkNPRE/mmK/kuIDmrL7kuLrnlKjmiLfmj5DkvpvkvIHkuJrlpJrmt7flkIjkupEg5LiA56uZ5byPRGV2T3BzIOiHquWKqOWMlui/kOe7tCDlrozlhajlvIDmupDnmoTkupHnrqHnkIblubPlj7Ag6Ieq5Yqo5YyW6L+Q57u05bmz5Y+wV2ViIFBlbnRlc3RpbmcgRnV6eiDlrZflhbgg5LiA5Liq5bCx5aSf5LqGICDorqHnrpfmnLrnvZHnu5wg6Ieq6aG25ZCR5LiL5pa55rOVIOWOn+S5puesrDbniYgg57yW56iL5L2c5LiaIFdpcmVzaGFya+WunumqjOaWh+aho+eahOe/u+ivkeWSjOino+etlCDkuK3mloflj6Tor5foh6rliqjkvZzor5fmnLrlmajkurog5bGM54K45aSpIOWfuuS6jnRlbnNvcmZsb3cxIDEgYXBpIOato+WcqOenr+aegee7tOaKpOWNh+e6p+S4rSDlv6tzdGFyIOS/neaMgeabtOaWsCBQeVF0IEV4YW1wbGVzIFB5UXTlkITnp43mtYvor5XlkozkvovlrZAgUHlRdDQgUHlRdDXmtbfph4/kuK3mlofpooTorq3nu4NBTEJFUlTmqKHlnovmsYnlrZfovazmi7zpn7MgcHlwaW55aW4g5pWw5o2u57uT5p6E5LiO566X5rOVIGxlZXRjb2RlIGxpbnRjb2Rl6aKY6KejICBQeXRvcmNo5qih5Z6L6K6t57uD5a6e55So5pWZ56iLIOS4remFjeWll+S7o+eggeWunuaXtuiOt+WPluaWsOa1qiDohb7orq8g55qE5YWN6LS56IKh56Wo6KGM5oOFIOmbhuaAnei3r+eahOWIhue6p+WfuumHkeihjOaDhVB5dGhvbueIrOiZqyBGbGFza+e9keermSDlhY3otLlTaGFkb3dTb2Nrc+i0puWPtyBzc3LorqLpmIUganNvbiDorqLpmIXlrp7miJgg5aSa56eN572R56uZIOeUteWVhuaVsOaNrueIrOiZqyDljIXlkKsg5reY5a6d5ZWG5ZOBIOW+ruS/oeWFrOS8l+WPtyDlpKfkvJfngrnor4Qg5LyB5p+l5p+lIOaLm+iBmOe9keermSDpl7Lpsbwg6Zi/6YeM5Lu75YqhIOWNmuWuouWbrSDlvq7ljZog55m+5bqm6LS05ZCnIOixhueTo+eUteW9sSDljIXlm77nvZEg5YWo5pmv572RIOixhueTo+mfs+S5kCDmn5DnnIHoja/nm5HlsYAg5pCc54uQ5paw6Ze7IOacuuWZqOWtpuS5oOaWh+acrOmHh+mbhiBmb2Zh6LWE5Lqn6YeH6ZuGIOaxvei9puS5i+WutiDlm73lrrbnu5/orqHlsYAg55m+5bqm5YWz6ZSu6K+N5pS25b2V5pWwIOicmOibm+azm+ebruW9lSDku4rml6XlpLTmnaEg6LGG55Oj5b2x6K+EIOaQuueoiyDlsI/nsbPlupTnlKjllYblupcg5a6J5bGF5a6iIOmAlOWutuawkeWuvyDvuI8g77iPIO+4jyDlvq7kv6HniKzomavlsZXnpLrpobnnm64gU1FMIOWuoeaguOafpeivouW5s+WPsOWbouWtkOe/u+ivkeWZqCDkuKrkurrlhbTotqPliLbkvZznmoTkuIDmrL7ln7rkuo5PQ1LmioDmnK/nmoTnv7vor5Hlmajoh6rliqjljJbov5Dnu7TlubPlj7Ag5Luj56CB5Y+K5bqU55So6YOo572yQ0kgQ0Qg6LWE5Lqn566h55CGQ01EQiDorqHliJLku7vliqHnrqHnkIblubPlj7AgU1FM5a6h5qC4IOWbnua7miDku7vliqHosIPluqYg56uZ5YaFV0lLSVNvdXJjZSBDb2RlIFNlY3VyaXR5IEF1ZGl0IOa6kOS7o+eggeWuieWFqOWuoeiuoSBFeHBodWIg5ryP5rSe5Yip55So6ISa5pys5bqTIOWMheaLrOeahOa8j+a0nuWIqeeUqOiEmuacrCDmnIDmlrDmt7vliqDmiJHnmoToh6rlrabnrJTorrAg57uI6Lqr5pu05pawIOW9k+WJjeS4k+azqFN5c3RlbeWfuuehgCBNTFN5cyDkvb/nlKjmnLrlmajlrabkuaDnrpfms5XlrozmiJDlr7kxMjMgNumqjOivgeeggeeahOiHquWKqOivhuWIq1B5dGhvbiDlvIDmupDpobnnm67kuYsg6Ieq5a2m57yW56iL5LmL6LevIOS/neWnhue6p+aVmeeoiyBBSeWunumqjOWupCDlrp3ol4/op4bpopEg5pWw5o2u57uT5p6EIOWtpuS5oOaMh+WNlyDmnLrlmajlrabkuaDlrp7miJgg5rex5bqm5a2m5Lmg5a6e5oiYIOe9kee7nOeIrOiZqyDlpKfljoLpnaLnu48g56iL5bqP5Lq655SfIOi1hOa6kOWIhuS6qyDkuK3mlofmlofmnKzliIbnsbvln7rkuo5weXRvcmNoIOW8gOeuseWNs+eUqCDmoLnmja7nvZHmmJPkupHpn7PkuZDnmoTmrYzljZUg5LiL6L29ZmxhY+aXoOaNn+mfs+S5kOWIsOacrOWcsOiFvuiur+S8mOWbvumrmOeyvuW6puWPjOWIhuaUr+S6uuiEuOajgOa1i+WZqOaWh+acrOe6oOmUmeetieaooeWei+WunueOsCDlvIDnrrHljbPnlKggMyDlpKnmjozmj6Hph4/ljJbkuqTmmJMg5oyB57ut5pu05pawIOS4reaWh+WIhuivjSDor43mgKfmoIfms6gg5ZG95ZCN5a6e5L2T6K+G5YirIOS+neWtmOWPpeazleWIhuaekCDmlrDor43lj5HnjrAg5YWz6ZSu6K+N55+t6K+t5o+Q5Y+WIOiHquWKqOaRmOimgSDmlofmnKzliIbnsbvogZrnsbsg5ou86Z+z566A57mBIOiHqueEtuivreiogOWkhOeQhuS4reaWh+WFrOW8gOiBiuWkqeivreaWmeW6k+ixhueTo+ivu+S5pueahOeIrOiZq+aAu+e7k+ais+eQhuiHqueEtuivreiogOWkhOeQhuW3peeoi+W4iCBOTFAg6ZyA6KaB56ev57Sv55qE5ZCE5pa56Z2i55+l6K+GIOWMheaLrOmdouivlemimCDlkITnp43ln7rnoYDnn6Xor4Yg5bel56iL6IO95Yqb562J562JIOaPkOWNh+aguOW/g+ernuS6ieWKm+S4reaWh+iHqueEtuivreiogOWkhOeQhuaVsOaNrumbhiDlubPml7blgZrlgZrlrp7pqoznmoTmnZDmlpkg5qyi6L+O6KGl5YWF5o+Q5Lqk5ZCI5bm2IOS4gOS4quWPr+S7peiHquW3sei/m+ihjOiuree7g+eahOS4reaWh+iBiuWkqeacuuWZqOS6uiDmoLnmja7oh6rlt7HnmoTor63mlpnorq3nu4Plh7roh6rlt7Hmg7PopoHnmoTogYrlpKnmnLrlmajkurog5Y+v5Lul55So5LqO5pm66IO95a6i5pyNIOWcqOe6v+mXruetlCDmmbrog73ogYrlpKnnrYnlnLrmma8g55uu5YmN5YyF5ZCrc2VxMnNlcSBzZXFHQU7niYjmnKwgdGYyIOeJiOacrCBweXRvcmNo54mI5pysIOiCoeelqOmHj+WMluahhuaetiDmlK/mjIHooYzmg4Xojrflj5bku6Xlj4rkuqTmmJPlvq7ljZrniKzomasg5oyB57ut57u05oqkICBCaWxpYmlsaSDnlKjmiLfniKzomasgZGVlcGlu5rqQ56e75qSNIERlYmlhbiBVYnVudHXkuIrmnIDlv6vnmoRRUSDlvq7kv6Hlronoo4XmlrnlvI8g5paw6Ze7572R6aG15q2j5paH6YCa55So5oq95Y+W5ZmoIEJldGEg54mIICBmbGFnIG9uIHBvc3Qg6Ieq5Yqo5pu05paw5Z+f5ZCN6Kej5p6Q5Yiw5pys5py6SVAg5pSv5oyBZG5zcG9kIOmYv+mHjEROUyBDbG91ZEZsYXJlIOWNjuS4uuS6kSBETlNDT00g5pys6aG555uu6ZKI5a+55a2X56ym5Z6L5Zu+54mH6aqM6K+B56CBIOS9v+eUqHRlbnNvcmZsb3flrp7njrDljbfnp6/npZ7nu4/nvZHnu5wg6L+b6KGM6aqM6K+B56CB6K+G5YirIG93bGxvb2sg5bCP6K+05pCc57Si5byV5pOO5Lit5paH6K+t6KiA55CG6Kej5rWL6K+E5Z+65YeGcHl0aG9u5Lit5paH5bqTIHB5dGhvbuS6uuW3peaZuuiDveWkp+aVsOaNruiHquWKqOWMluaOpeWPo+a1i+ivleW8gOWPkSDkuabnsY3kuIvovb3lj4pweXRob27lupPmsYfmgLtjaGluYSB0ZXN0aW5nIGdpdGh1YiBpbyAyIDE55paw5Z6L5Yag54q255eF5q+S55ar5oOF5pe26Ze05bqP5YiX5pWw5o2u5LuT5bqTUHl0aG9uIOm7kemtlOazleaJi+WGjOWNlemYtuautemAmueUqOebruagh+ajgOa1i+WZqOS4gOS4quaLjeeFp+WBmumimOeoi+W6jyDovpPlhaXkuIDlvKDljIXlkKvmlbDlraborqHnrpfpopjnmoTlm77niYcg6L6T5Ye66K+G5Yir5Ye655qE5pWw5a2m6K6h566X5byP5Lul5Y+K6K6h566X57uT5p6cIHZpZGVvIGRvd25sb2FkIELnq5nop4bpopHkuIvovb3kuK3mloflkb3lkI3lrp7kvZPor4bliKsgVGVuc29yRmxvdyBQeXRob24g5Lit5paH5pWw5o2u57uT5p6E5ZKM566X5rOV5pWZ56iLIOmqjOivgeeggeivhuWIqyDorq3nu4NQeXRob27niKzomavlrp7miJgg5qih5ouf55m76ZmG5ZCE5aSn572R56uZIOWMheWQq+S9huS4jemZkOS6jiDmu5HlnZfpqozor4Eg5ou85aSa5aSaIOe+juWboiDnmb7luqYgYmlsaWJpbGkg5aSn5LyX54K56K+EIOa3mOWunSDlpoLmnpzllpzmrKLor7dzdGFydCDvuI/lrabml6DmraLkuIvovb3lmagg5oWV6K++5LiL6L295ZmoIE1vb2PkuIvovb0g5oWV6K++572R5LiL6L29IOS4reWbveWkp+WtpuS4i+i9vSDniLHor77nqIvkuIvovb0g572R5piT5LqR6K++5aCC5LiL6L29IOWtpuWgguWcqOe6v+S4i+i9vSDotoXmmJ/lrabkuaDpgJrkuIvovb0g5pSv5oyB6KeG6aKRIOivvuS7tuWQjOaXtuS4i+i9veS4gOS4qumrmOe6p3dlYuebruW9lSDmlofku7bmiavmj4/lt6Xlhbcg5Yqf6IO95bCG5Lya5by65LqORGlyQnVzdGVyIERpcnNlYXJjaCBjYW5zaW5hIOW+oeWJkSDmkJzntKLmiYDmnInkuK3mlodOTFDmlbDmja7pm4Yg6ZmE5bi455So6Iux5paHTkxQ5pWw5o2u6ZuG5Lit5paH5a6e5L2T6K+G5Yir5LiO5YWz57O75o+Q5Y+WMiAxOeaWsOWei+WGoOeKtueXheavkueWq+aDheWunuaXtueIrOiZq+WPimdpdGh1YiByZWxlYXNlIGFyY2hpdmXku6Xlj4rpobnnm67mlofku7bnmoTliqDpgJ/pobnnm67lronljZPlupTnlKjlronlhajlrabkuaDmipPlj5blpKfph4/lhY3otLnku6PnkIYgaXAg5o+Q5Y+W5pyJ5pWIIGlwIOS9v+eUqFJvQkVSVGHkuK3mlofpooTorq3nu4PmqKHlnosgUm9CRVJUYSBmb3IgQ2hpbmVzZSDnlKjkuo7orq3nu4PkuK3oi7Hmloflr7nor53ns7vnu5/nmoTor63mlpnlupPmlY/mhJ/or43ov4fmu6TnmoTlh6Dnp43lrp7njrAg5p+QMXfor43mlY/mhJ/or43lupPnroDljZXmmJPnlKjnmoRQeXRob27niKzomavmoYbmnrYgUVHkuqTmtYHnvqQgNTk3NTEgNTYg5L2/55SoQmVydCBFUk5JRSDov5vooYzkuK3mlofmlofmnKzliIbnsbvkuLogQ1NBUFAg6KeG6aKR6K++56iL5o+Q5L6b5a2X5bmVIOe/u+ivkSBQUFQgTGFiIFB5VG9yY2gg5a6Y5pa55Lit5paH5pWZ56iL5YyF5ZCrIDYg5YiG6ZKf5b+r6YCf5YWl6Zeo5pWZ56iLIOW8uuWMluaVmeeoiyDorqHnrpfmnLrop4bop4kg6Ieq54S26K+t6KiA5aSE55CGIOeUn+aIkOWvueaKl+e9kee7nCDlvLrljJblrabkuaAg5qyi6L+OIFN0YXIgRm9yayDlhZzlk6Xlh7rlk4EgXFx1MDAzY+S4gOacrOW8gOa6kOeahE5MUOWFpemXqOS5puexjSDlm77lg4/nv7vor5Eg5p2h5Lu2R0FOIEFJ57uY55S755SoUmVzbmV0MSAxIEdQVOaQreW7uuS4gOS4queOqeeOi+iAheiNo+iAgOeahEFJ5ZCE56eN5ryP5rSecG9jIEV4cOeahOaUtumbhuaIlue8luWGmeaWl+WcsOS4u0FJVnVsbWFwIOaYr+S4gOasviB3ZWIg5ryP5rSe5omr5o+P5ZKM6aqM6K+B5bel5YW3IOWPr+WvuSB3ZWJhcHBzIOi/m+ihjOa8j+a0nuaJq+aPjyDlubbkuJTlhbflpIfmvI/mtJ7pqozor4Hlip/og73mj5DkvpvotoXpgY4gNSDlgIvph5Hono3os4fmlpkg5Y+w6IKh54K65Li7IOavj+WkqeabtOaWsCBmaW5taW5kIGdpdGh1YiBpbyDmlbDmja7mjqXlj6Mg55m+5bqmIOiwt+atjCDlpLTmnaEg5b6u5Y2a5oyH5pWwIOWuj+inguaVsOaNriDliKnnjofmlbDmja4g6LSn5biB5rGH546HIOWNg+mHjOmprCDni6zop5Llhb3lhazlj7gg5paw6Ze76IGU5pKt5paH5a2X56i/IOW9seinhuelqOaIv+aVsOaNriDpq5jmoKHlkI3ljZUg55ar5oOF5pWw5o2uIFB5T25lIOS4gOasvue7meWKm+eahG9uZWRyaXZl5paH5Lu2566h55CGIOWIhuS6q+eoi+W6jyDkvb/nlKjlvIDlj5HnmoTnlKjkuo7ov4XpgJ/mkK3lu7rlubbkvb/nlKggV2ViSG9vayDov5vooYzoh6rliqjljJbpg6jnvbLlkozov5Dnu7Qg5pSv5oyBIEdpdGh1YiBHaXRMYWIgR29ncyBHaXRPc2Mg6Lef5oiR5LiA6LW35YaZTWFrZWZpbGXph43liLbniYggcHl0aG9u6Ieq5Yqo5YyW6L+Q57u0IOaKgOacr+S4juacgOS9s+Wunui3tSDkuabkuK3npLrkvovlj4rmoYjkvovmupDnoIHoh6rnhLbor63oqIDlpITnkIblrp7pqowgc291Z2915pWw5o2u6ZuGIFRGIElERiDmlofmnKzliIbnsbsg6IGa57G7IOivjeWQkemHjyDmg4XmhJ/or4bliKsg5YWz57O75oq95Y+W562J5b6u5L+h5YWs5LyX5bmz5Y+wIFB5dGhvbiDlvIDlj5HljIUgREVQUkVDQVRFRCBXZWJsb2dpY+S4gOmUrua8j+a0nuajgOa1i+W3peWFtyBWMSA1IOabtOaWsOaXtumXtCAyIDIgNzMg5a6M5aSH5LyY6ZuF55qE5b6u5L+h5YWs5LyX5Y+35o6l5Y+jIOWOn+eUn+aUr+aMgeWQjOatpSDljY/nqIvkvb/nlKgg5pys56iL5bqP5peo5Zyo5Li65a6J5YWo5bqU5oCl5ZON5bqU5Lq65ZGY5a+5TGludXjkuLvmnLrmjpLmn6Xml7bmj5Dkvpvkvr/liKkg5a6e546w5Li75py65L6nQ2hlY2tsaXN055qE6Ieq5Yqo5YWo6Z2i5YyW5qOA5rWLIOagueaNruajgOa1i+e7k+aenOiHquWKqOaVsOaNruiBmuWQiCDov5vooYzpu5HlrqLmlLvlh7vot6/lvoTmuq/mupAgUHlDaGFybSDkuK3mlofmjIfljZcg5a6J6KOFIOegtOinoyDmlYjnjocg5oqA5ben57G75Ly85oyJ6ZSu57K+54G155qE6byg5qCH6ZSu55uY5b2V5Yi25ZKM6Ieq5Yqo5YyW5pON5L2cIOaooeaLn+eCueWHu+WSjOmUruWFpUdQVDIgZm9yIENoaW5lc2UgY2hpdGNoYXQg55So5LqO5Lit5paH6Zey6IGK55qER1BUMuaooeWeiyDlrp7njrDkuoZEaWFsb0dQVOeahE1NSeaAneaDsyDkuK3ljY7kurrmsJHlhbHlkozlm73lm73lrrbmoIflh4YgR0IgVCAyMjYg6KGM5pS/5Yy65YiS5Luj56CB5Z+65LqOcHl0aG9u55qE6YeP5YyW5Lqk5piT5bmz5Y+w5Lit5paH6K+t6Z+z6K+G5Yir5Z+65LqOIE9uZUJvdCDmoIflh4bnmoQgUHl0aG9uIOW8guatpSBRUSDmnLrlmajkurrmoYbmnrZSZWFsIFdvcmxkIE1hc2tlZCBGYWNlIERhdGFzZXQg5Y+j572p5Lq66IS45pWw5o2u6ZuGIFZ1bGZvY3VzIOaYr+S4gOS4qua8j+a0numbhuaIkOW5s+WPsCDlsIbmvI/mtJ7njq/looMgZG9ja2VyIOmVnOWDjyDmlL7lhaXljbPlj6/kvb/nlKgg5byA566x5Y2z55SoIOiwt+atjCDnmb7luqYg5b+F5bqU5Zu+54mH5LiL6L29IOWfuuS6juaWuemdoueahOaDheaEn+WIhuaekCDkvb/nlKhQeVRvcmNo5a6e546wICDmt7HluqblrabkuaDkuI7orqHnrpfmnLrop4bop4kg6YWN5aWX5Luj56CBQVJU546v5aKD5LiL6Ieq5Yqo5YyW6ISx5aOz5pa55qGI5Yip55So572R57uc5LiK5YWs5byA55qE5pWw5o2u5p6E5bu65LiA5Liq5bCP5Z6L55qE6K+B5Yi455+l6K+G5Zu+6LCxIOefpeivhuW6k+S4reaWh+i1hOa6kOeyvumAiSDlrpjmlrnnvZHnq5kg5a6J6KOF5pWZ56iLIOWFpemXqOaVmeeoiyDop4bpopHmlZnnqIsg5a6e5oiY6aG555uuIOWtpuS5oOi3r+W+hCBRUee+pCAxNjcxMjI4NjEg5YWs5LyX5Y+3IOejkOWIm0FJIOW+ruS/oee+pOS6jOe7tOeggSB3d3cgdGVuc29yZmxvd25ld3MgY29tIFByZSBUcmFpbmVkIENoaW5lc2UgWExOZXQg5Lit5paHWExOZXTpooTorq3nu4PmqKHlnosg5paw5rWq5b6u5Y2aUHl0aG9uIFNES+acieWFs2J1cnBzdWl0ZeeahOaPkuS7tiDpnZ7llYblupcg5paH56ug5Lul5Y+K5L2/55So5oqA5ben55qE5pS26ZuGIOatpOmhueebruS4jeWGjeaPkOS+m2J1cnBzdWl0ZeegtOino+aWh+S7tiDlpoLpnIDopoHor7flnKjljZrlrqJtcnhuIG5ldOS4i+i9vVB5dGhvbjPnvJblhpnnmoRDTVPmvI/mtJ7mo4DmtYvmoYbmnrbln7rkuo5kamFuZ2/nmoTlt6XkvZzmtYHlvJXmk44g5bel5Y2VIO+4jyDlk5Tlk6nkupEg5LiN5pSv5oyB5Lu75oSP5paH5Lu255qE5YWo6YCf5LiK5Lyg5LiO5LiL6L295b6u5L+hU0RLIOWMheaLrOW+ruS/oeaUr+S7mCDlvq7kv6HlhazkvJflj7cg5b6u5L+h55m76ZmGIOW+ruS/oea2iOaBr+WkhOeQhuetieS4reaWh+iHqueEtuivreiogOeQhuino+WgoeWekuacuiDkupHmoYzpnaLoh6rliqjljJbov5Dnu7Qg5a6h6K6hIOW9leWDjyDmlofku7bnrqHnkIYgc2Z0cOS4iuS8oCDlrp7ml7bnm5Hmjqcg5b2V5YOP5Zue5pS+IOe9kemhteeJiHJ6IHN65LiK5Lyg5LiL6L29IOWKqOaAgeWPo+S7pCBkamFuZ2/ovazmjaLkuK3lm73nn6XnvZEgQ0FKIOagvOW8j+aWh+eMruS4uiBQREYg5L2b57O76L2s5o2iIOaIkOWKn+S4juWQpiDnmobmmK/njoTlraYgSGFuTFDkvZzogIXnmoTmlrDkuaYg6Ieq54S26K+t6KiA5aSE55CG5YWl6ZeoIOivpue7hueslOiusCDkuJrnlYzoia/lv4PkuYvkvZwg5Lmm5Lit5LiN5piv5p6v54el5peg5ZGz55qE5YWs5byP572X5YiXIOiAjOaYr+eUqOeZveivnemYkOi/sOeahOmAmuS/l+aYk+aHgueahOeul+azleaooeWeiyDku47ln7rmnKzmpoLlv7Xlh7rlj5Eg6YCQ5q2l5LuL57uN5Lit5paH5YiG6K+NIOivjeaAp+agh+azqCDlkb3lkI3lrp7kvZPor4bliKsg5L+h5oGv5oq95Y+WIOaWh+acrOiBmuexuyDmlofmnKzliIbnsbsg5Y+l5rOV5YiG5p6Q6L+Z5Yeg5Liq54Ot6Zeo6Zeu6aKY55qE566X5rOV5Y6f55CG5LiO5bel56iL5a6e546wICBQeXRob24zIOe9kee7nOeIrOiZq+WunuaImCDpg6jliIblkKvor6bnu4bmlZnnqIsg54yr55y8IOiFvuiur+inhumikSDosYbnk6Mg56CU5oub572RIOW+ruWNmiDnrJTotqPpmIHlsI/or7Qg55m+5bqm54Ot54K5IELnq5kgQ1NETiDnvZHmmJPkupHpmIXor7sg6Zi/6YeM5paH5a2mIOeZvuW6puiCoeelqCDku4rml6XlpLTmnaEg5b6u5L+h5YWs5LyX5Y+3IOe9keaYk+S6kemfs+S5kCDmi4nli74g5pyJ6YGTIHVuc3BsYXNoIOWunuS5oOWDpyDmsb3ovabkuYvlrrYg6Iux6ZuE6IGU55uf55uS5a2QIOWkp+S8l+eCueivhCDpk77lrrYgTFBM6LWb56iLIOWPsOmjjiDmoqblubvopb/muLgg6Zi06Ziz5biI6JeP5a6d6ZiBIOWkqeawlCDniZvlrqLnvZEg55m+5bqm5paH5bqTIOedoeWJjeaVheS6iyDnn6XkuY4gV2lzaOW+ruS/oeWFrOS8l+WPt+aWh+eroOeahOeIrOiZqyBQeXRob24gV2Vi5byA5Y+R5a6e5oiYIOS5puS4rea6kOeggeS4gOebtOWPr+eUqOeahEdvQWdlbnQg5Lya5a6a5pe25omr5o+P5Y+v55So55qEZ29vZ2xlIGdhZSBpcCDmj5Dkvpvlj6/oh6rliqjljJbojrflj5ZpcOi/kOihjOeahOeJiOacrOWxguWJquaenSDpgJrpgZPliarmnp0g55+l6K+G6JK46aaPIOS4reaWh+WRveWQjeWunuS9k+ivhuWIqyDljIXmi6zlpJrnp43mqKHlnosgSE1NIENSRiBCaUxTVE0gQmlMU1RNIENSRueahOWFt+S9k+WunueOsCAgVGhlIFdheSB0byBHbyDkuK3mlofor5HmnKwg5Lit5paH5q2j5byP5ZCNIEdvIOWFpemXqOaMh+WNlyDosKLosKIgU29sdXRpb25zIHRvIExlZXRDb2RlIGJ5IEdvIDEgJSB0ZXN0IGNvdmVyYWdlIHJ1bnRpbWUgYmVhdHMgMSAlIExlZXRDb2RlIOmimOino+S4gOasvui9u+mHj+e6pyDpq5jmgKfog70g5Yqf6IO95by65aSn55qE5YaF572R56m/6YCP5Luj55CG5pyN5Yqh5ZmoIOaUr+aMgXRjcCB1ZHAgc29ja3M1IGh0dHDnrYnlh6DkuY7miYDmnInmtYHph4/ovazlj5Eg5Y+v55So5p2l6K6/6Zeu5YaF572R572R56uZIOacrOWcsOaUr+S7mOaOpeWPo+iwg+ivlSBzc2jorr/pl64g6L+c56iL5qGM6Z2iIOWGhee9kWRuc+ino+aekCDlhoXnvZFzb2NrczXku6PnkIbnrYnnrYkg5bm25bim5pyJ5Yqf6IO95by65aSn55qEd2Vi566h55CG56uvIEdv6K+t6KiA6auY57qn57yW56iLIOW8gOa6kOWbvuS5piDmtrXnm5ZDR08gR2/msYfnvJbor63oqIAgUlBD5a6e546wIFByb3RvYnVm5o+S5Lu25a6e546wIFdlYuahhuaetuWunueOsCDliIbluIPlvI/ns7vnu5/nrYnpq5jpmLbkuLvpopgg5a6M56i/IOaYr+S4gOS4qui3qOW5s+WPsOeahOW8uuWKoOWvhuaXoOeJueW+geeahOS7o+eQhui9r+S7tiDpm7bphY3nva4g566X5rOV5qih5p2/IOacgOenkeWtpueahOWIt+mimOaWueW8jyDmnIDlv6vpgJ/nmoTliLfpopjot6/lvoQg5L2g5YC85b6X5oul5pyJIOeZvuW6pue9keebmOS4jemZkOmAn+WuouaIt+erryBnb2xhbmcgcXQ1IOi3qOW5s+WPsOWbvuW9oueVjOmdouaYr2dvbGFuZ+WunueOsOeahOmrmOaAp+iDvWh0dHAgaHR0cHMgd2Vic29ja2V0IHRjcCBzb2NrczXku6PnkIbmnI3liqHlmagg5pSv5oyB5YaF572R56m/6YCPIOmTvuW8j+S7o+eQhiDpgJrorq/liqDlr4Yg5aSc6K+7IOmAmui/hyBiaWxpYmlsaSDlnKjnur/nm7Tmkq3nmoTmlrnlvI/liIbkuqsgR28g55u45YWz55qE5oqA5pyv6K+d6aKYIOavj+WkqeWkp+WutuWcqOW+ruS/oSB0ZWxlZ3JhbSBTbGFjayDkuIrlj4rml7bmsp/pgJrkuqTmtYHnvJbnqIvmioDmnK/or53popggIOaUr+aMgeWkmuWutuS6keWtmOWCqOeahOS6keebmOezu+e7nyDov5nph4zmmK/lhpnljZrlrqLnmoTlnLDmlrkgSGFsZnJvc3QgRmllbGQg5Yaw6Zyc5LmL5ZywTGFudGVybuWumOaWueeJiOacrOS4i+i9vSDok53nga8g57+75aKZIOS7o+eQhiDnp5HlrabkuIrnvZEg5aSW572RIOWKoOmAn+WZqCDmoq/lrZAg6Lev55Sx5Z+65LqOZ2luIHZ1ZeaQreW7uueahOWQjuWPsOeuoeeQhuezu+e7n+ahhuaetiDpm4bmiJBqd3TpibTmnYMg5p2D6ZmQ566h55CGIOWKqOaAgei3r+eUsSDliIbpobXlsIHoo4Ug5aSa54K555m75b2V5oum5oiqIOi1hOa6kOadg+mZkCDkuIrkvKDkuIvovb0g5Luj56CB55Sf5oiQ5ZmoIOihqOWNleeUn+aIkOWZqCDpgJrnlKjlt6XkvZzmtYHnrYnln7rnoYDlip/og70g5LqU5YiG6ZKf5LiA5aWXQ1VSROWJjeWQjuerr+S7o+eggSDnm65WVUUz54mI5pys5q2j5Zyo6YeN5p6EIOasoui/jmlzc3Vl5ZKMcHIg5YiG5biD5byP54is6Jmr566h55CG5bmz5Y+wIOaUr+aMgeS7u+S9leivreiogOWSjOahhuaetkdvbGFuZ+agh+WHhuW6kyDlr7nkuo7nqIvluo/lkZjogIzoqIAg5qCH5YeG5bqT5LiO6K+t6KiA5pys6Lqr5ZCM5qC36YeN6KaBIOWug+WlveavlOS4gOS4queZvuWuneeusSDog73kuLrlkITnp43luLjop4HnmoTku7vliqHmj5Dkvpvlroznvo7nmoTop6PlhrPmlrnmoYgg5Lul56S65L6L6amx5Yqo55qE5pa55byP6K6y6KejR29sYW5n55qE5qCH5YeG5bqTIOWkqeeUqEdv5Yqo5omL5YaZIOS7jumbtuWunueOsOezu+WIl+aYr+S4gOS4qumrmOaAp+iDveS4lOS9juaNn+iAl+eahCBnb3JvdXRpbmUg5rGgIOaciSDmnIkg6K6+6K6h5qih5byPIEdvbGFuZ+WunueOsCDnoJTno6jorr7orqHmqKHlvI8g6K+75Lmm56yU6K6wR29sYW5n5a6e546w55qE5Z+65LqOYmVlZ2/moYbmnrbnmoTmjqXlj6PlnKjnur/mlofmoaPnrqHnkIbns7vnu5/pq5jmgKfog73lvIDmupBSVFNQ5rWB5aqS5L2T5pyN5Yqh5ZmoIOWfuuS6jmdv6K+t6KiA56CU5Y+RIOe7tOaKpOWSjOS8mOWMliBSVFNQ5o6o5qih5byP6L2s5Y+RIFJUU1Dmi4nmqKHlvI/ovazlj5Eg5piv5LiA5Liq6auY5oCn6IO9IOi9u+mHj+e6pyDpnZ7pmLvloZ7nmoTkuovku7bpqbHliqggR28g572R57uc5qGG5p62IOWfuuS6jkdpbiBWdWUgRWxlbWVudCBVSeeahOWJjeWQjuerr+WIhuemu+adg+mZkOeuoeeQhuezu+e7n+iEmuaJi+aetiDljIXlkKvkuoYg5aSa56ef5oi355qE5pSv5oyBIOWfuuehgOeUqOaIt+euoeeQhuWKn+iDvSBqd3TpibTmnYMg5Luj56CB55Sf5oiQ5ZmoIFJCQUPotYTmupDmjqfliLYg6KGo5Y2V5p6E5bu6IOWumuaXtuS7u+WKoeetiSAz5YiG6ZKf5p6E5bu66Ieq5bex55qE5Lit5ZCO5Y+w6aG555uuIOaWh+aho+iTnemyuOaZuuS6kemFjee9ruW5s+WPsCBCbHVlS2luZyBDTURCIOS7iuaXpeeDreamnCDkuIDkuKrojrflj5blkITlpKfng63pl6jnvZHnq5nng63pl6jlpLTmnaHnmoTogZrlkIjnvZHnq5kg5L2/55SoR2/or63oqIDnvJblhpkg5aSa5Y2P56iL5byC5q2l5b+r6YCf5oqT5Y+W5L+h5oGvIOmihOiniCBtbyBmaXNo5LiA5p2h5ZG95Luk56a757q/5a6J6KOF6auY5Y+v55Soa3ViZXJuZXRlcyAzbWlu6KOF5a6MIDcgTSAxIOW5tOivgeS5piDnlJ/kuqfnjq/looPnqLPlpoLogIHni5fpmL/ph4zlt7Tlt7TlvIDmupDnmoTkuIDmrL7nroDljZXmmJPnlKgg5Yqf6IO95by65aSn55qE5re35rKM5a6e6aqM5rOo5YWl5bel5YW3ICBHb+ivreiogOWbm+WNgeS6jOeroOe7jyDor6bnu4borrLov7BHb+ivreiogOinhOiMg+S4juivreazlee7huiKguWPiuW8gOWPkeS4reW4uOingeeahOivr+WMuiDpgJrov4fnoJTor7vmoIflh4blupPnrYnnu4/lhbjku6PnoIHorr7orqHmqKHlvI8g5ZCv5Y+R6K+76ICF5rex5Yi755CG6KejR2/or63oqIDnmoTmoLjlv4PmgJ3nu7Qg6L+b5YWlR2/or63oqIDlvIDlj5HnmoTmm7Tpq5jpmLbmrrUgIO+4j+S4gOS4qui9u+W3p+eahOe9kee7nOa3t+a3huS7o+eQhiDln7rkuo5Hb2xhbmfovbvph4/nuqdUQ1Dlubblj5HmnI3liqHlmajmoYbmnrblrprml7bku7vliqHnrqHnkIbns7vnu59LdWJlT3BlcmF0b3Ig5piv5LiA5Liq5byA5rqQ55qE6L276YeP57qnIEt1YmVybmV0ZXMg5Y+R6KGM54mIIOS4k+azqOS6juW4ruWKqeS8geS4muinhOWIkiDpg6jnvbLlkozov5DokKXnlJ/kuqfnuqfliKvnmoQgSzhzIOmbhue+pCDmnKzns7vnu5/mmK/pm4blt6XljZXnu5/orqEg5Lu75Yqh6ZKp5a2QIOadg+mZkOeuoeeQhiDngbXmtLvphY3nva7mtYHnqIvkuI7mqKHniYjnrYnnrYnkuo7kuIDouqvnmoTlvIDmupDlt6XljZXns7vnu58g5b2T54S25Lmf5Y+v5Lul56ew5LmL5Li65bel5L2c5rWB5byV5pOOIOiHtOWKm+S6juWHj+Wwkei3qOmDqOmXqOS5i+mXtOeahOayn+mAmiDoh6rliqjku7vliqHnmoTmiafooYwg5o+Q5Y2H5bel5L2c5pWI546H5LiO5bel5L2c6LSo6YePIOWHj+WwkeS4jeW/heimgeeahOW3peS9nOmHj+S4juS6uuS4uuWHuumUmeeOhyBHb+WunueOsOeahFRyb2phbuS7o+eQhiDmlK/mjIHlpJrot6/lpI3nlKgg6Lev55Sx5Yqf6IO9IENETuS4rei9rCBTaGFkb3dzb2Nrc+a3t+a3huaPkuS7tiDlpJrlubPlj7Ag5peg5L6d6LWWIEdv6K+t5rOV5qCR5YWl6ZeoIOW8gOWQr+iHquWItue8lueoi+ivreiogOWSjOe8luivkeWZqOS5i+aXhSDlvIDmupDlhY3otLnlm77kuaYgR2/or63oqIDov5vpmLYg5o6M5o+h5oq96LGh6K+t5rOV5qCRIEdv6K+t6KiAQVNUIOWHueivreiogCDkuIDmrL7lj6/lhajlubPlj7Dov5DooYznmoTmtY/op4jlmajmlbDmja7lr7zlh7rop6Plr4blt6XlhbcgR29sYW5n55u45YWzIOWuoeeov+i/m+W6pjggJSBHb+ivreazlSBHb+W5tuWPkeaAneaDsyBHb+S4jndlYuW8gOWPkSBHb+W+ruacjeWKoeiuvuaWveetiUp1cGl0ZXLmmK/mlpfpsbzlvIDmupDnmoTpnaLlkJHmnI3liqHmsrvnkIbnmoRHb2xhbmflvq7mnI3liqHmoYbmnrZFbGFzdGljc2VhcmNoIOWPr+inhuWMlkRhc2hCb2FyZCDmlK/mjIFFc+ebkeaOpyDlrp7ml7bmkJzntKIgSW5kZXggdGVtcGxhdGXlv6vmjbfmm7/mjaLkv67mlLkg57Si5byV5YiX6KGo5L+h5oGv5p+l55yLIFNRTCBjb252ZXJ0cyB0byBEU0znrYkgIOS7jumXrumimOWIh+WFpSDkuLLov54gR28g6K+t6KiA55u45YWz55qE5omA5pyJ55+l6K+GIOiejeS8mui0r+mAmiBnb2xhbmcgZGVzaWduIGdvIHF1ZXN0aW9uc1dlQ2hhdCBTREsgZm9yIEdvIOW+ruS/oVNESyDnroDljZUg5piT55SoIGdvIGZhc3RkZnMg5piv5LiA5Liq566A5Y2V55qE5YiG5biD5byP5paH5Lu257O757ufIOengeacieS6keWtmOWCqCDlhbfmnInml6DkuK3lv4Mg6auY5oCn6IO9IOmrmOWPr+mdoCDlhY3nu7TmiqTnrYnkvJjngrkg5pSv5oyB5pat54K557ut5LygIOWIhuWdl+S4iuS8oCDlsI/mlofku7blkIjlubYg6Ieq5Yqo5ZCM5q2lIOiHquWKqOS/ruWkjSBNYXN0ZXJpbmcgR08g5Lit5paH6K+R5pysIOeOqei9rCBHTyDkupHljp/nlJ/kuJTmmJPnlKjnmoTlupTnlKjnrqHnkIblubPlj7AgR28gV2ViIOWfuuehgCDmmK/kuIDlpZfpkojlr7kgR29vZ2xlIOWHuuWTgeeahCBHbyDor63oqIDnmoTop4bpopHor63pn7PmlZnnqIsg5Li76KaB6Z2i5ZCR5a6M5oiQIEdvIOe8lueoi+WfuuehgCDmlZnnqIvlkI7luIzmnJvov5vkuIDmraXkuobop6PmnInlhbMgR28gV2ViIOW8gOWPkeeahOWtpuS5oOiAhSDkuK3mloflkI0g5oKf56m6IEFQSSDnvZHlhbMg5piv5LiA5Liq5Z+65LqOIEdvbGFuZ+W8gOWPkeeahOW+ruacjeWKoee9keWFsyDog73lpJ/lrp7njrDpq5jmgKfog70gSFRUUCBBUEkg6L2s5Y+RIOacjeWKoee8luaOkiDlpJrnp5/miLfnrqHnkIYgQVBJIOiuv+mXruadg+mZkOaOp+WItuetieebrueahCDmi6XmnInlvLrlpKfnmoToh6rlrprkuYnmj5Lku7bns7vnu5/lj6/ku6Xoh6rooYzmianlsZUg5bm25LiU5o+Q5L6b5Y+L5aW955qE5Zu+5b2i5YyW6YWN572u55WM6Z2iIOiDveWkn+W/q+mAn+W4ruWKqeS8geS4mui/m+ihjCBBUEkg5pyN5Yqh5rK755CGIOaPkOmrmCBBUEkg5pyN5Yqh55qE56iz5a6a5oCn5ZKM5a6J5YWo5oCnICDpm4blkIjlpJrlrrYgQVBJIOeahOaWsOS4gOS7o+WbvuW6ik1JVOivvueoiyBEaXN0cmlidXRlZCBTeXN0ZW1zIOWtpuS5oOWSjOe/u+ivkUdv6K+t6KiA5Zyj57uP5Lit5paH54mIIOWPquaOpeaUtlBSIElzc3Vl6K+35o+Q5Lqk5YiwZ29sYW5nIGNoaW5hIGdvcGwgemggdHJvamFu5aSa55So5oi3566h55CG6YOo572y56iL5bqPIOaUr+aMgXdlYumhtemdoueuoeeQhkJvb2tTdGFjayDln7rkuo5NaW5Eb2Mg5L2/55SoQmVlZ2/lvIDlj5HnmoTlnKjnur/mlofmoaPnrqHnkIbns7vnu58g5Yqf6IO957G75Ly8R2l0Ym9va+WSjOeci+S6kSB3ZWl4aW4gd2VjaGF0IOW+ruS/oeWFrOS8l+W5s+WPsCDlvq7kv6HkvIHkuJrlj7cg5b6u5L+h5ZWG5oi35bmz5Y+wIOW+ruS/oeaUr+S7mCBnbyBnb2xhbmcgc2RrICDok53nnLzkupHnm5ggRXllYmx1ZSBDbG91ZCBTdG9yYWdlIOivreiogOmrmOaAp+iDvee8lueoiyBHbyDor63oqIDpmbfpmLEgR290Y2hhcyBUcmFwcyDkvb/nlKggWE1pbmQg6K6w5b2VIExpbnV4IOaTjeS9nOezu+e7nyDnvZHnu5wgQyBHb2xhbmcg5Lul5Y+K5pWw5o2u5bqT55qE5LiA5Lqb6K6+6K6hY3FodHRw55qEZ29sYW5n5a6e546wIOi9u+mHjyDljp/nlJ/ot6jlubPlj7AgbXFhbnTmmK/kuIDmrL7ln7rkuo5Hb2xhbmfor63oqIDnmoTnroDmtIEg6auY5pWIIOmrmOaAp+iDveeahOWIhuW4g+W8j+W+ruacjeWKoeahhuaetuWfuuS6jnJlYWN0IG5vZGUganMgZ2/lvIDlj5HnmoTlvq7llYbln44g5ZCr5b6u5L+h5bCP56iL5bqPIE1NIFdpa2kg5LiA5Liq6L276YeP57qn55qE5LyB5Lia55+l6K+G5YiG5Lqr5LiO5Zui6Zif5Y2P5ZCM6L2v5Lu2IOWPr+eUqOS6juW/q+mAn+aehOW7uuS8geS4miBXaWtpIOWSjOWboumYn+efpeivhuWIhuS6q+W5s+WPsCDpg6jnvbLmlrnkvr8g5L2/55So566A5Y2VIOW4ruWKqeWboumYn+aehOW7uuS4gOS4quS/oeaBr+WFseS6qyDmlofmoaPnrqHnkIbnmoTljY/kvZznjq/looMgR28g6K+t6KiA5Lit5paH572RIEdvbGFuZ+S4reaWh+ekvuWMuiBHb+ivreiogOWtpuS5oOWbreWcsCDmupDnoIHln7rkuo4gR2luIOi/m+ihjOaooeWdl+WMluiuvuiuoeeahCBBUEkg5qGG5p62IOWwgeijheS6huW4uOeUqOWKn+iDvSDkvb/nlKjnroDljZUg6Ie05Yqb5LqO6L+b6KGM5b+r6YCf55qE5Lia5Yqh56CU5Y+RIOavlOWmgiDmlK/mjIEgY29ycyDot6jln58gand0IOetvuWQjemqjOivgSB6YXAg5pel5b+X5pS26ZuGIHBhbmljIOW8guW4uOaNleiOtyB0cmFjZSDpk77ot6/ov73ouKogcHJvbWV0aGV1cyDnm5HmjqfmjIfmoIcgc3dhZ2dlciDmlofmoaPnlJ/miJAgdmlwZXIg6YWN572u5paH5Lu26Kej5p6QIGdvcm0g5pWw5o2u5bqT57uE5Lu2IGdvcm1nZW4g5Luj56CB55Sf5oiQ5bel5YW3IGdyYXBocWwg5p+l6K+i6K+t6KiAIGVycm5vIOe7n+S4gOWumuS5iemUmeivr+eggSBnUlBDIOeahOS9v+eUqCDnrYnnrYkgc3luY2TmmK/kuIDmrL7lvIDmupDnmoTku6PnoIHpg6jnvbLlt6Xlhbcg5a6D5YW35pyJ566A5Y2VIOmrmOaViCDmmJPnlKjnrYnnibnngrkg5Y+v5Lul5o+Q6auY5Zui6Zif55qE5bel5L2c5pWI546HIOS4gOasvueUsSBZU1JDIOW8gOa6kOeahOS4u+acuuWFpeS+teajgOa1i+ezu+e7n2dvbGFuZ+mdouivlemimOmbhuWQiOi/meaYr+S4gOS4quWPr+S7peivhuWIq+inhumikeivremfs+iHquWKqOeUn+aIkOWtl+W5lVNSVOaWh+S7tueahOW8gOa6kCBXaW5kb3dzIEdVSSDova/ku7blt6Xlhbcg5LiA5qy+5YaF572R57u85ZCI5omr5o+P5bel5YW3IOaWueS+v+S4gOmUruiHquWKqOWMliDlhajmlrnkvY3mvI/miavmiavmj48g5piv5LiA5Liq55So5LqO5Zyo5Lik5LiqcmVkaXPkuYvpl7TlkIzmraXmlbDmja7nmoTlt6Xlhbcg5ruh6Laz55So5oi36Z2e5bi454G15rS755qE5ZCM5q2lIOi/geenu+mcgOaxgiBPdmVybG9yZOaYr+WTlOWTqeWTlOWTqeWfuuS6jkdv6K+t6KiA57yW5YaZ55qEbWVtY2FjaGXlkoxyZWRpcyBjbHVzdGVy55qE5Luj55CG5Y+K6ZuG576k566h55CG5Yqf6IO9IOiHtOWKm+S6juaPkOS+m+iHquWKqOWMlumrmOWPr+eUqOeahOe8k+WtmOacjeWKoeino+WGs+aWueahiCBTdGFjayBSUEMg5Lit5paH56S65L6LIOaVmeeoiyDotYTmlpkg5rqQ56CB6Kej6K+7SUNNUOa1gemHj+S8quijhei9rOWPkeW3peWFt0ZyZWVkb23mmK/kuIDkuKrln7rkuo7lha3ovrnlvaLmnrbmnoTnmoTmoYbmnrYg5Y+v5Lul5pSv5pKR5YWF6KGA55qE6aKG5Z+f5qih5Z6L6IyD5byPICBHbzLnvJbnqIvmjIfljZcg5byA5rqQ5Zu+5LmmIOmHjeeCueiusuino0dvMuaWsOeJueaApyDku6Xlj4pHbzHmlZnnqIvkuK3ovoPlsJHmtonlj4rnmoTnibnmgKfor63oqIDpq5jmgKfog73liIbor41nb2xhbmflhpnnmoRJTeacjeWKoeWZqCDmnI3liqHnu4Tku7blvaLlvI8gIOe7k+W3tCDkuK3mlofliIbor43nmoRHb2xhbmfniYjmnKx4b3Jt5piv5LiA5Liq566A5Y2V6ICM5by65aSn55qER2/or63oqIBPUk3lupMg6YCa6L+H5a6D5Y+v5Lul5L2/5pWw5o2u5bqT5pON5L2c6Z2e5bi4566A5L6/IOacrOW6k+aYr+WfuuS6juWOn+eJiHhvcm3nmoTlrprliLblop7lvLrniYjmnKwg5Li6eG9ybeaPkOS+m+exu+S8vGliYXRpc+eahOmFjee9ruaWh+S7tuWPiuWKqOaAgVNRTOaUr+aMgSDmlK/mjIFBY2l0dmVSZWNvcmTmk43kvZzkuIDkuKogR28g6K+t6KiA5a6e546w55qE5b+r6YCfIOeos+WumiDlhoXltYznmoQgayB2IOaVsOaNruW6kyDpq5jmgKfog73ooajmoLzmlbDmja7lr7zlh7rlmajln7rkuo5Hb2xhbmfnmoTlvIDmupDnpL7ljLrns7vnu58g54mI5pys572R5piT5LqR6Z+z5LmQbmNt5paH5Lu25qC85byP6L2s5o2iIGdvIOWunueOsOeahOWOi+a1i+W3peWFtyBhYiBsb2N1c3QgSm1ldGVy5Y6L5rWL5bel5YW35LuL57uNIOWNleWPsOacuuWZqDEgd+i/nuaOpeWOi+a1i+WunuaImCDmipPljIXmiKrlj5bpobnnm67kuK3nmoTmlbDmja7lupPor7fmsYLlubbop6PmnpDmiJDnm7jlupTnmoTor63lj6UgIEdv5LiT5a6257yW56iLIEdv6K+t6KiA5b+r6YCf5YWl6ZeoIOi9u+advui/m+mYtiBcXHUwMDNjXFx1MDAzY+iHquW3seWKqOaJi+WGmWRvY2tlciDmupDnoIFHbyDmr4/ml6XkuIDlupNrdW5wZW5n5piv5LiA5LiqR29sYW5n57yW5YaZ55qE5byA5rqQUE9D5qGG5p62IOW6kyDku6XliqjmgIHpk77mjqXlupPnmoTlvaLlvI/mj5DkvpvlkITnp43or63oqIDosIPnlKgg6YCa6L+H5q2k6aG555uu5Y+v5b+r6YCf5byA5Y+R5ryP5rSe5qOA5rWL57G755qE57O757ufIHZ1ZSBqcyBlbGVtZW505qGG5p62IGdvbGFuZyBiZWVnb+ahhuaetiDlvIDlj5HnmoTov5Dnu7Tlj5HluIPns7vnu58g5pSv5oyBZ2l0IGplbmtpbnPniYjmnKzlj5HluIMgZ28gc3NoIEJU5Lik56eN5paH5Lu25Lyg6L6T5pa55byP6YCJ5oupIOaUr+aMgemDqOe9suWJjeWHhuWkh+S7u+WKoeWSjOmDqOe9suWQjuS7u+WKoemSqeWtkOWHveaVsCBHbyDku47lhaXpl6jliLDlrp7miJgg5a2m5Lmg56yU6K6wIOS7jumbtuW8gOWni+WtpiBHbyBHaW4g5qGG5p62IOWfuuacrOivreazleWMheaLrCAyNiDkuKpEZW1vIEdpbiDmoYbmnrbljIXmi6wgR2luIOiHquWumuS5iei3r+eUsemFjee9riBHaW4g5L2/55SoIExvZ3J1cyDov5vooYzml6Xlv5forrDlvZUgR2luIOaVsOaNrue7keWumuWSjOmqjOivgSBHaW4g6Ieq5a6a5LmJ6ZSZ6K+v5aSE55CGIEdvIGdSUEMgSGVsbG8gV29ybGQg5oyB57ut5pu05paw5LitIEdvIOWtpuS5oOS5i+i3ryBHbyDlvIDlj5HogIXljZrlrqIgR28g5b6u5L+h5YWs5LyX5Y+3IEdvIOWtpuS5oOi1hOaWmSDmlofmoaMg5Lmm57GNIOinhumikSDlvq7kv6EgV2VDaGF0IOaUr+S7mOWunSBBbGlQYXkg55qER2/niYjmnKxTREsg5p6B566AIOaYk+eUqOeahOiBmuWQiOaUr+S7mFNESyBHbyBieSBFeGFtcGxlIOmAmui/h+S+i+WtkOWtpiBHb2xhbmdQUEdvIEpvYuaYr+S4gOasvuWPr+inhuWMlueahCDlpJrkurrlpJrmnYPpmZDnmoQg5LiA5Lu75Yqh5aSa5py65omn6KGM55qE5a6a5pe25Lu75Yqh566h55CG57O757ufIOmHh+eUqGdvbGFuZ+W8gOWPkSDlronoo4Xmlrnkvr8g6LWE5rqQ5raI6ICX5bCRIOaUr+aMgeWkp+W5tuWPkSDlj6/lkIzml7bnrqHnkIblpJrlj7DmnI3liqHlmajkuIrnmoTlrprml7bku7vliqEgR29sYW5n5a6e546w55qESVDku6PnkIbmsaDmmK/kuIDmrL7nlKhHb+ivreiogOW8gOWPkeeahHdlYuW6lOeUqOahhuaetiBBUEnnibnmgKfnsbvkvLzkuo5Ub3JuYWRv5bm25LiU5oul5pyJ5q+UVG9ybmFkb+abtOWlveeahOaAp+iDvSAg6Ieq5bex5Yqo5omL5YaZSmF2YeiZmuaLn+acuiDpmo/kuabmupDku6PnoIHmlK/ku5jlrp0gQWxpUGF5IFNESyBmb3IgR28g6ZuG5oiQ566A5Y2VIOWKn+iDveWujOWWhCDmjIHnu63mm7TmlrAg5pSv5oyB5YWs6ZKl6K+B5Lmm5ZKM5pmu6YCa5YWs6ZKl6L+b6KGM562+5ZCN5ZKM6aqM562+ICBBUkNISVZFRCBHZXBoIOi/t+mcp+mAmuW4ruWKqeS9oOWwhuacrOWcsOerr+WPo+aatOmcsuWcqOWklue9kSDmlK/mjIFUQ1AgVURQIOW9k+eEtuS5n+aUr+aMgUhUVFAg5rex5YWlR2/lubblj5HnvJbnqIvnoJTorqjor77ml6DnirbmgIHlrZDln5/lkI3niIbnoLTlt6XlhbfmiYvmnLrlj7fnoIHlvZLlsZ7lnLDkv6Hmga/lupMg5omL5py65Y+35b2S5bGe5Zyw5p+l6K+iIHBob25lIGRhdCDmnIDlkI7mm7TmlrAgMiAyMeW5tCA25pyIIGdvbGFuZ+WfuuS6jndlYnNvY2tldOWNleWPsOacuuWZqOaUr+aMgeeZvuS4h+i/nuaOpeWIhuW4g+W8j+iBiuWkqSBJTSDns7vnu5/ln7rkuo5tb25nb2RiIG9wbG9n55qE6ZuG576k5aSN5Yi25bel5YW3IOWPr+S7pea7oei2s+i/geenu+WSjOWQjOatpeeahOmcgOaxgiDov5vkuIDmraXlrp7njrDngb7lpIflkozlpJrmtLvlip/og70gR2luIEdvcm3lvIDlj5FHb2xhbmcgQVBJ5b+r6YCf5byA5Y+R6ISa5omL5p62566A5Y2V5Y+v5L+h6LWW55qE5Lu75Yqh566h55CG5bel5YW3R2/or63oqIDlrp7kvovmlZnnqIvku47lhaXpl6jliLDov5vpmLYg5YyF5ous5Z+656GA5bqT5L2/55SoIOiuvuiuoeaooeW8jyDpnaLor5XmmJPplJnngrkg5bel5YW357G7IOWvueaOpeesrOS4ieaWueetieaOiOadg+ahhuaetueugOS9k+S4reaWh+e/u+ivkSDoh6rliqjmipPlj5Z0Z+mikemBkyDorqLpmIXlnLDlnYAg5YWs5byA5LqS6IGU572R5LiK55qEc3Mgc3NyIHZtZXNzIHRyb2phbuiKgueCueS/oeaBryDogZrlkIjljrvph43lkI7mj5DkvpvoioLngrnliJfooajovbvph4/nuqcgZ28g5Lia5Yqh5qGG5p62ICDlk6rlkJLnm5Hmjqcg5LiA56uZ5byP6L2755uR5o6n6L276L+Q57u057O757ufIOaUr+aMgeezu+e7n+eKtuaAgSBUQ1AgUGluZyDnm5HmjqfmiqXoraYg5ZG95Luk5om56YeP5omn6KGM5ZKM6K6h5YiS5Lu75YqhIEdvIOivreiogOWumOaWueaVmeeoi+S4reaWh+eJiOW3peeoi+W4iOefpeivhueuoeeQhuezu+e7nyDln7rkuo5nb2xhbmcgZ2/or63oqIAgYmVlZ2/moYbmnrYg5q+P5Liq6KGM5Lia6YO95pyJ6Ieq5bex55qE55+l6K+G566h55CG57O757ufIGVuZ2luZWVyY21z5peo5Zyo5Li65Zyf5pyo5bel56iL5biI5Lus5omT6YCg5LiA5qy+6YCC55So55qE5Z+65LqOd2Vi55qE55+l6K+G566h55CG57O757ufIOWug+aXouWPr+S7peeUqOS6jueuoeeQhuS4quS6uueahOmhueebrui1hOaWmSDkuZ/lj6/ku6XnlKjkuo7nrqHnkIbpobnnm67lm6LpmJ/otYTmlpkg5a6D5pei5Y+v5Lul6L+Q6KGM5LqO5Liq5Lq655S16ISRIOS5n+WPr+S7peaUvuWIsOacjeWKoeWZqOS4iiDmlK/mjIHmj5Dlj5bnoIHliIbkuqvmlofku7Ygb25seW9mZmljZeWunuaXtuaWh+aho+WNj+S9nCDnm7TmjqXlnKjnur/nvJbovpFkd2fmlofku7Ygb2ZmaWNl5paH5qGjIOWcqOe6v+WIqeeUqG1pbmRvY+WIm+S9nOS9oOeahOS5puexjSDpmIXop4hQREbmlofku7Yg6YCa55So55qE5Lia5Yqh5rWB56iL6K6+572uIOaJi+acuuerr+mFjeWll+Wwj+eoi+W6jyDlvq7kv6HmkJzntKLigJznj6DkuInop5Lorr7ku6PigJ3miJbigJzpnZLlsJHlhL/kuabnlLvigJ3ljbPlj6/lkbzlh7rlsI/nqIvluo8g6L6555WM5omT54K55ZCO55qE6Ieq5Yqo5YyW5riX6YCP5bel5YW35LiA5Liq6ZuG5a6h5qC4IOaJp+ihjCDlpIfku73lj4rnlJ/miJDlm57mu5ror63lj6Xkuo7kuIDouqvnmoRNeVNRTOi/kOe7tOW3peWFt+axieWtl+i9rOaLvOmfsyBHb+i1hOa6kOeyvumAieS4reaWh+eJiCDlkKvkuK3mloflm77kuablpKflhagg6K+t6KiA5a6e546w55qEIFJlZGlzIOacjeWKoeWZqOWSjOWIhuW4g+W8j+mbhue+pCDotoXlhahnb2xhbmfpnaLor5XpopjlkIjpm4YgZ29sYW5n5a2m5Lmg5oyH5Y2XIGdvbGFuZ+efpeivhuWbvuiwsSDlhaXpl6jmiJDplb/ot6/nur8g5LiA5Lu95ra155uW5aSn6YOo5YiGZ29sYW5n56iL5bqP5ZGY5omA6ZyA6KaB5o6M5o+h55qE5qC45b+D55+l6K+GIOW4uOeUqOesrOS4ieaWueW6kyBteXNxbCBtcSBlcyByZWRpc+etiSDmnLrlmajlrabkuaDlupMg566X5rOV5bqTIOa4uOaIj+W6kyDlvIDmupDmoYbmnrYg6Ieq54S26K+t6KiA5aSE55CGbmxw5bqTIOe9kee7nOW6kyDop4bpopHlupMg5b6u5pyN5Yqh5qGG5p62IOinhumikeaVmeeoiyDpn7PpopHpn7PkuZDlupMg5Zu+5b2i5Zu+54mH5bqTIOeJqeiBlOe9keW6kyDlnLDnkIbkvY3nva7kv6Hmga8g5bWM5YWl5byP6ISa5pys5bqTIOe8luivkeWZqOW6kyDmlbDmja7lupMg6YeR6J6N5bqTIOeUteWtkOmCruS7tuW6kyDnlLXlrZDkuabnsY0g5YiG6K+NIOaVsOaNrue7k+aehCDorr7orqHmqKHlvI8g5Y67aHRtbCB0YWfmoIfnrb7nrYkgZ2/lrabkuaAgZ2/pnaLor5Vnb+ivreiogOaJqeWxleWMhSDmlLbpm4bkuIDkupvluLjnlKjnmoTmk43kvZzlh73mlbAg6L6F5Yqp5pu05b+r55qE5a6M5oiQ5byA5Y+R5bel5L2cIOW5tuWHj+WwkemHjeWkjeS7o+eggeeZvueBteW/q+S8oCDln7rkuo5Hb+ivreiogOeahOmrmOaAp+iDvSDmiYvmnLrnlLXohJHotoXlpKfmlofku7bkvKDovpPnpZ7lmagg5bGA5Z+f572R5YWx5Lqr5paH5Lu25pyN5Yqh5ZmoIExBTiBsYXJnZSBmaWxlIHRyYW5zZmVyIHRvb2wg5LiA5Liq5Z+65LqO5LqR5a2Y5YKo55qE572R55uY57O757ufIOeUqOS6juiHquW7uuengeS6uue9keebmOaIluS8geS4mue9keebmCBnb+WIhuW4g+W8j+acjeWKoeWZqCDln7rkuo7lhoXlrZhtbW/kuKrkurrljZrlrqLlvq7kv6HlsI/nqIvluo/mnI3liqHnq68gU0RLIGZvciBHb2xhbmcg5o6n5Yi25Y+w6aKc6Imy5riy5p+T5bel5YW35bqTIOaUr+aMgTE26ImyIDI1NuiJsiBSR0LoibLlvanmuLLmn5PovpPlh7og5L2/55So57G75Ly85LqOIFByaW50IFNwcmludGYg5YW85a655bm25pSv5oyBIFdpbmRvd3Mg546v5aKD55qE6Imy5b2p5riy5p+T5Z+65LqOIElvQyDnmoQgR28g5ZCO56uv5LiA56uZ5byP5byA5Y+R5qGG5p62IHYycmF5IHdlYiBtYW5hZ2VyIOaYr+S4gOS4qnYycmF555qE6Z2i5p2/IOS5n+aYr+S4gOS4qumbhue+pOeahOino+WGs+aWueahiCDlkIzml7blop7liqDkuobmtYHph4/mjqfliLYg6LSm5Y+3566h55CGIOmZkOmAn+etieWKn+iDvSBrZXkgYWRtaW4gcGFuZWwgd2ViIGNsdXN0ZXIg6ZuG576kIHByb3h5U2VydmVyU2NhbuS4gOasvuS9v+eUqEdvbGFuZ+W8gOWPkeeahOmrmOW5tuWPkee9kee7nOaJq+aPjyDmnI3liqHmjqLmtYvlt6Xlhbcg5pivaHR0cCBjbGllbnTpoobln5/nmoTnkZ7lo6vlhpvliIAg5bCP5benIOW8uuWkpyDnioDliKkg5YW35L2T55So5rOV5Y+v55yL5paH5qGjIOWmguS9v+eUqOi/t+aDkeaIluiAhUFQSeeUqOW+l+S4jeeIvemDveWPr+aPkGlzc3Vlc1RjcFJvdXRlIFRDUCDlsYLnmoTot6/nlLHlmagg5a+55LqOIFRDUCDov57mjqXoh6rliqjku47lpJrkuKrnur/ot68g55S15L+hIOiBlOmAmiDnp7vliqgg5aSa5Liq5Z+f5ZCN6Kej5p6Q57uT5p6c5Lit6YCJ5oup5pyA5LyY57q/6LevIEJpZnJvc3Qg6Z2i5ZCR55Sf5Lqn546v5aKD55qEIE15U1FMIOWQjOatpeWIsFJlZGlzIE1vbmdvREIgQ2xpY2tIb3VzZSBNeVNRTOetieacjeWKoeeahOW8guaehOS4remXtOS7tuW6lOeUqOe9keWFsyDmj5Dkvpvlv6vpgJ8g5a6J5YWo55qE5bqU55So5Lqk5LuYIOi6q+S7veiupOivgSBXQUYgQ0MgSFRUUFPku6Xlj4pBQ01F6Ieq5Yqo6K+B5LmmIEEgdGVsZWdyYW0gYm90IGZvciByc3MgcmVhZGVyIOS4gOS4quaUr+aMgeW6lOeUqOWGhemYheivu+eahCBUZWxlZ3JhbSBSU1MgQm90IFJFU1RmdWwgQVBJIOaWh+aho+eUn+aIkOW3peWFtyDmlK/mjIHlkowgUnVieSDnrYnlpKfpg6jliIbor63oqIAg5Z+65LqOZ2luIGdvcm3lvIDlj5HnmoTkuKrkurrljZrlrqLpobnnm67ln7rkuo5Hb+ivreiogOeahOWbveWvhlNNMiBTTTMgU000566X5rOV5bqTIEdvbGFuZyDorr7orqHmqKHlvI/kuIDkuKrpmL/ph4zkupHnm5jliJfooajnqIvluo8g5LiA5qy+5bCP5ben55qE5Z+65LqOR2/mnoTlu7rnmoTlvIDlj5HmoYbmnrYg5Y+v5Lul5b+r6YCf5p6E5bu6QVBJ5pyN5Yqh5oiW6ICFV2Vi572R56uZ6L+b6KGM5Lia5Yqh5byA5Y+RIOmBteW+qlNPTElE6K6+6K6h5Y6f5YiZ5bm25Y+R57yW56iL5a6e5oiYIOesrDLniYggR28g5a2m5LmgIEdvIOi/m+mYtiBHbyDlrp7nlKjlt6XlhbfnsbsgR28ga2l0IEdvIE1pY3JvIOW+ruacjeWKoeWunui3tSBHbyDmjqjpgIHln7rkuo5ERETnmoRvMm/nmoTkuJrliqHmqKHlnovlj4rln7rnoYAg5L2/55SoR29sYW5nIGdSUEMgVGhyaWZ05a6e546wU2hhcmluZ2FuIOWGmei9ruecvCDmmK/kuIDkuKrln7rkuo5nb2xhbmfnmoTmtYHph4/lvZXliLblm57mlL7lt6Xlhbcg6YCC5ZCI6aG555uu6YeN5p6EIOWbnuW9kua1i+ivleetiSDnmb7luqbkupHnvZHnm5jniKzomavln7rkuo5iZWVnb+eahOi/m+mUgOWtmOezu+e7nyBUZWFXZWIg5Y+v6KeG5YyW55qEV2Vi5Luj55CG5pyN5YqhIERFTU8gdGVhb3MgY24gIOeZveW4veWtkOWuieWFqOW8gOWPkeWunuaImCDphY3lpZfku6PnoIHmipbpn7PmjqjojZAg5pCc57Si6aG16KeG6aKR5YiX6KGo6KeG6aKR54is6Jmr5pa55qGIIOWfuuS6jmFwcCDomZrmi5/mnLrmiJbnnJ/mnLog55u45YWz5oqA5pyvIGdvbGFuZyBhZGLkuIDmrL7nlLLmlrnotYTkuqflt6HoiKrmiavmj4/ns7vnu58g57O757uf5a6a5L2N5piv5Y+R546w6LWE5LqnIOi/m+ihjOerr+WPo+eIhuegtCDluK7liqnkvIHkuJrmm7Tlv6vlj5HnjrDlvLHlj6Pku6Tpl67popgg5Li76KaB5Yqf6IO95YyF5ousIOi1hOS6p+aOoua1iyDnq6/lj6PniIbnoLQg5a6a5pe25Lu75YqhIOeuoeeQhuWQjuWPsOivhuWIqyDmiqXooajlsZXnpLrmj5Dkvpvlvq7kv6Hnu4jnq6/niYjmnKwg5b6u5L+h5ZG95Luk6KGM54mI5pys6IGK5aSp5Yqf6IO9IOW+ruS/oeacuuWZqOS6uiDvuI8g5LqS6IGU572R5pyA5YWo5aSn5Y6C5oqA5pyv5YiG5LqrUFBUIOaMgee7reabtOaWsOS4rSDlkITlpKfmioDmnK/kuqTmtYHkvJog5rS75Yqo6LWE5paZ5rGH5oC7IOWmgiBRQ29uIOWFqOeQg+i/kOe7tOaKgOacr+Wkp+S8miBHREcg5YWo55CD5oqA5pyv6aKG5a+85Yqb5bOw5LyaIOWkp+WJjeerr+Wkp+S8miDmnrbmnoTluIjls7DkvJog5pWP5o235byA5Y+RRGV2T3BzIE9wZW5SZXN0eSBFbGFzdGljIOasoui/jiBQUiBJc3N1ZXPml6XmnKzpurvlsIbliqnmiYsg54mM5pWIIOmYsuWuiCDorrDniYwg5pSv5oyB6ZuA6a2CIOWkqeWHpCDlvIDmupDlrqLmnI3ns7vnu59HT+ivreiogOW8gOWPkUdPIEZMWSDlhY3otLnlrqLmnI3ns7vnu5/kuIDkuKrmn6Xor6JJUOWcsOeQhuS/oeaBr+WSjENETuacjeWKoeaPkOS+m+WVhueahOemu+e6v+e7iOerr+W3peWFtyDmmK/kuIDkuKrnlKjkuo7ns7vnu5/ph43mnoQg57O757uf6L+B56e75ZKM57O757uf5YiG5p6Q55qE55Ge5aOr5Yab5YiAIOWug+WPr+S7peWIhuaekOS7o+eggeS4reeahOa1i+ivleWdj+WRs+mBkyDmqKHlnZfljJbliIbmnpAg6KGM5pWw57uf6K6hIOWIhuaekOiwg+eUqOS4juS+nei1liBHaXQg5YiG5p6Q5Lul5Y+K6Ieq5Yqo5YyW6YeN5p6E562JIOS4gOS4quebtOaSreW9leWItuW3peWFt01hc3RlcmluZyBHbyDnrKzkuozniYjkuK3mlofniYjmnaXooq0g5riX6YCP5rWL6K+V5oOF5oql5pS26ZuG5bel5YW35YiG5biD5byP5a6a5pe25Lu75Yqh6LCD5bqm5bmz5Y+w6auY5bqm5qih5Z2X5YyWIOmBteW+qiBLSVNT5Y6f5YiZ55qE5Yy65Z2X6ZO+5byA5Y+R5qGG5p62Z29sYW5n54mI5pys55qEaGFuZ291dCDluIzmnJvog73nnIHkupvlhoXlrZgg5L2/55So5LqG6Ieq5bex5YaZ55qES2Fma2EgbGliIOiZmiDkuI3ov4fmiJHku6zlnKjnlJ/kuqfnjq/looPlt7Lnu4/kvb/nlKjov5Ex5bm0IGthZmthIOeJiOacrOS7jiA5IDHliLAyIOmDveWcqOS9v+eUqCDnm67liY3mg4XlhrXnqLPlrpog5ZCe5ZCQ6YeP5Zyo5q+P5aSpMiDkur/mnaHku6XkuIogR28g6K+t6KiAIFdlYiDlupTnlKjlvIDlj5Hns7vliJfmlZnnqIsg5LuO5paw5omL5Yiw5Y+M5omL5q6L5bqfaXJpcyDmoYbmnrbnmoTlkI7lj7BhcGnpobnnm67nroDljZXlpb3nlKjnmoRERE5TIOiHquWKqOabtOaWsOWfn+WQjeino+aekOWIsOWFrOe9kUlQIOaUr+aMgemYv+mHjOS6kSDohb7orq/kupFkbnNwb2QgQ2xvdWRmbGFyZSDljY7kuLrkupEgIOiHquW3seWKqOaJi+WunueOsEx1YSDpmo/kuabmupDku6PnoIFwaHDnm7Tmkq1nb+ebtOaSrSDnn63op4bpopEg55u05pKt5bim6LSnIOS7v+avlOW/gyDnjI7muLggdHTor63pn7PogYrlpKkg576O5aWz57qm546pIOmZqueOqeezu+e7n+a6kOeggeW8gOm7kSDnuqbnjqnmupDnoIEg56S+5Yy65byA5rqQIOS6keWOn+eUn+eahOWkmuS6keWSjOa3t+WQiOS6keiejeWQiOW5s+WPsCBKaWFqdW7nmoTnvJbnqIvpmo/mg7NHb2xhbmfor63oqIDnpL7ljLog6IW+6K6v6K++5aCCIOe9keaYk+S6keivvuWggiDlrZfoioLmlZnogrLor77nqItQUFTlj4rku6PnoIHln7rkuo5HRiBHbyBGcmFtZSDnmoTlkI7lj7DnrqHnkIbns7vnu5/luKbkvaDkuobop6PkuIDkuItHb2xhbmfnmoTluILlnLrooYzmg4VteXNxbOihqOe7k+aehOiHquWKqOWQjOatpeW3peWFtyDnm67liY3lj6rmlK/mjIHlrZfmrrUg57Si5byV55qE5ZCM5q2lIOWIhuWMuuetiemrmOe6p+WKn+iDveaaguS4jeaUr+aMgSDln7rkuo5LdWJlcm5ldGVz55qEUGFhU+W5s+WPsOa1geWqkuS9k05ldEZsaXjop6PplIHmo4DmtYvohJrmnKznqLPlrprliIbmlK8yIDkgWCDniYjmnKzlt7Lmm7TmlrAg55SxIEdvbGFuZ+ivreiogOa4uOaIj+acjeWKoeWZqCDnu7TmiqQg5YWo55CD5pyN5ri45oiP5pyN5Yqh5Zmo5Y+K5Yy65Z+f5pyN5qGG5p62IOebruWJjeWNj+iuruaUr+aMgXdlYnNvY2tldCBLQ1AgVENQ5Y+KUlBDIOmHh+eUqOeKtuaAgeWQjOatpSDluKflkIzmraXlhoXmtYsg5oS/5pmvIOaJk+mAoE1NT+WkmuS6uuernuaKgOa4uOaIj+ahhuaetiDlip/og73mjIHnu63mm7TmlrDkuK0g5Z+65LqOIEdvbGFuZyDnsbvkvLznn6XkuY7nmoTnp4HmnInpg6jnvbLpl67nrZTlupTnlKgg5YyF5ZCr6Zeu562UIOivhOiuuiDngrnotZ4g566h55CG5ZCO5Y+w562J5Yqf6IO95YWo5paw55qE5byA5rqQ5ryP5rSe5rWL6K+V5qGG5p62IOWunueOsHBvY+WcqOe6v+e8lui+kSDov5DooYwg5om56YeP5rWL6K+VIOS9v+eUqOaWh+ahoyBYQVBJIE1BTkFHRVIg5LiT5Lia5a6e55So55qE5byA5rqQ5o6l5Y+j566h55CG5bmz5Y+wIOS4uueoi+W6j+W8gOWPkeiAheaPkOS+m+S4gOS4queBtea0uyDmlrnkvr8g5b+r5o2355qEQVBJ566h55CG5bel5YW3IOiuqUFQSeeuoeeQhuWPmOeahOabtOWKoOa4heaZsCDmmI7mnJcg5aaC5p6c5L2g6KeJ5b6XeEFwaeWvueS9oOacieeUqOeahOivnSDliKvlv5jkuobnu5nmiJHku6zngrnkuKrotZ7lk6YgcXHljY/orq7nmoRnb2xhbmflrp7njrAg56e75qSN5LqObWlyYWlnb+eJiOacrOaegeeugOW3peS9nOa1geW8leaTjuWFqOW5s+WPsEdv5byA5rqQ5YaF572R5riX6YCP5omr5o+P5Zmo5qGG5p62IFdpbmRvd3MgTGludXggTWFj5YaF572R5riX6YCPIOS9v+eUqOWug+WPr+i9u+advuS4gOmUruaJuemHj+aOoua1i0PmrrUgQuautSBB5q615a2Y5rS75Li75py6IOmrmOWNsea8j+a0nuajgOa1i01TMTcgMSBTbWJHaG9zdCDov5znqIvmiafooYxTU0ggV2lucm0g5a+G56CB54iG56C056uv5Y+j5omr5o+P5pyN5Yqh6K+G5YirUG9ydFNjYW7mjIfnurnor4bliKvlpJrnvZHljaHkuLvmnLog56uv5Y+j5omr5o+P5pyN5Yqh6K+G5YirUG9ydFNjYW4gaWlraXJhIEJhaWR1UENTIEdv5Y6f54mI5Z+656GA5LiK6ZuG5oiQ5LqG5YiG5Lqr6ZO+5o6lIOenkuS8oOmTvuaOpei9rOWtmOWKn+iDvSBl562+5a6d5a6J5YWo5Zui6Zif56ev57Sv5Y2B5Yeg5bm055qE5a6J5YWo57uP6aqMIOmDveWwhuWvueWklumAkOatpeW8gOaUviDpppblvIDnmoRFaG9uZXnmrLrpqpfpmLLlvqHns7vnu58g6K+l57O757uf5piv5Z+65LqO5LqR5Y6f55Sf55qE5qy66aqX6Ziy5b6h57O757ufIOS5n+aYr+S4mueVjOWUr+S4gOW8gOa6kOeahOWvueagh+WVhuS4muezu+e7n+eahOS6p+WTgSDmrLrpqpfpmLLlvqHns7vnu5/pgJrov4fpg6jnvbLpq5jkuqTkupLpq5jku7/nnJ/onJznvZDlj4rmtYHph4/ku6PnkIbovazlj5Eg5YaN57uT5ZCI6Ieq56CU5a+G562+5Y+K6K+x6aW1IOWwhuaUu+WHu+iAheaUu+WHu+W8leWvvOWIsOicnOe9kOS4rei+vuWIsOaJsOS5seW8leWvvOS7peWPiuW7tui/n+aUu+WHu+eahOaViOaenCDlj6/ku6XlvojlpKfnqIvluqbkuIrkv53miqTkuJrliqHnmoTlronlhagg5oqk572R5b+F5aSH6Imv6I2v5ryC5Lqu55qER2/or63oqIDpgJrnlKjlkI7lj7DnrqHnkIbmoYbmnrYg5YyF5ZCr6K6h5YiS5Lu75YqhIE15U1FM566h55CGIFJlZGlz566h55CGIEZUUOeuoeeQhiBTU0jnrqHnkIYg5pyN5Yqh5Zmo566h55CGIENhZGR56YWN572uIOS6keWtmOWCqOeuoeeQhuetieWKn+iDvSAg5b6u5L+h5pSv5LuYIFdlQ2hhdCBQYXkgU0RLIGZvciBHb2xhbmfnlKjkuo7nm5Hmjqfns7vnu5/nmoTml6Xlv5fph4fpm4ZhZ2VudCDlj6/ml6DnvJ3lr7nmjqVvcGVuIGZhbGNvbumYv+mHjOW3tOW3tG15c3Fs5pWw5o2u5bqTYmlubG9n55qE5aKe6YeP6K6i6ZiFIOa2iOi0uee7hOS7tiBDYW5hbCDnmoQgZ28g5a6i5oi356uvIGdpdGh1YiBjb20gYWxpYmFiYSBjYW5hbCDnlKjkuo7mr5TovoMy5LiqcmVkaXPmlbDmja7mmK/lkKbkuIDoh7Qg5pSv5oyB5Y2V6IqC54K5IOS4u+S7jiDpm4bnvqTniYgg5Lul5Y+K5aSa56eNcHJveHkg5pSv5oyB5ZCM5p6E5Lul5Y+K5byC5p6E5a+55q+UIHJlZGlz55qE54mI5pys5pSv5oyBMiB4IDUgeCDkvb/nlKhnbyBtaWNyb+W+ruacjeWKoeWunueOsOeahOWcqOe6v+eUteW9semZouiuouelqOezu+e7n+WQjuerr+S4gOermeW8j+W+ruacjeWKoeahhuaetiDmj5DkvptBUEkgd2ViIHdlYnNvY2tldCBSUEMg5Lu75Yqh6LCD5bqmIOa2iOaBr+a2iOi0ueacjeWKoeWZqOe6ouiTneWvueaKl+i3qOW5s+WPsOi/nOaOp+W3peWFt0ludGVyY2hhaW4gcHJvdG9jb2wg6Leo6ZO+5Y2P6K6u566A5Y2V5piT55SoIOi2s+Wkn+i9u+mHjyDmgKfog73lpb3nmoQgR29sYW5nIOW6k+S4gOS4qmdvIGVjaG8gdnVlIOW8gOWPkeeahOW/q+mAnyDnroDmtIEg576O6KeCIOWJjeWQjuerr+WIhuemu+eahOS4quS6uuWNmuWuouezu+e7nyBibG9nIOS5n+WPr+aWueS+v+S6jOasoeW8gOWPkeS4ukNNUyDlhoXlrrnnrqHnkIbns7vnu58g5ZKM5ZCE56eN5LyB5Lia6Zeo5oi3572R56uZIOato+WcqOabtOaWsOadg+mZkOeuoeeQhiBoYXV0aOmhueebriDkuI3mmK/kuIDkuKrliY3nq69vcuWQjuWPsOahhuaetiDogIzmmK/kuIDkuKrpm4bmiJDmnYPpmZDnrqHnkIYg6I+c5Y2V6LWE5rqQ566h55CGIOWfn+euoeeQhiDop5LoibLnrqHnkIYg55So5oi3566h55CGIOe7hOe7h+aetuaehOeuoeeQhiDmk43kvZzml6Xlv5fnrqHnkIbnrYnnrYnnmoTlv6vpgJ/lvIDlj5HlubPlj7DvvI4gaGF1dGjmmK/kuIDkuKrln7rnoYDkuqflk4Eg5Zyo6L+Z5Liq5Z+656GA5Lqn5ZOB5LiKIOagueaNruS4muWKoemcgOaxgiDlv6vpgJ/nmoTlvIDlj5HlupTnlKjmnI3liqHvvI7otKblj7cgYWRtaW4g5a+G56CBIDEyMzQ1NumAmueUqOeahOaVsOaNrumqjOivgeS4jui/h+a7pOW6kyDkvb/nlKjnroDljZUg5YaF572u5aSn6YOo5YiG5bi455So6aqM6K+BIOi/h+a7pOWZqCDmlK/mjIHoh6rlrprkuYnpqozor4Hlmagg6Ieq5a6a5LmJ5raI5oGvIOWtl+autee/u+ivkSBDVEYgQVdEIEF0dGFjayB3aXRoIERlZmVuc2Ug57q/5LiL6LWb5bmz5Y+wIEFXRCBwbGF0Zm9ybSDmrKLov44gU3RhciDok53psrjmmbrkupHlrrnlmajnrqHnkIblubPlj7AgQmx1ZUtpbmcgQ29udGFpbmVyIFNlcnZpY2Ug56iL5bqP5ZGY5aaC5L2V5LyY6ZuF55qE5oyj6Zu26Iqx6ZKxIDIg54mIIOWNh+e6p+S4uuWwj+S5puS6hiDkuIDkuKogUEhQIOW+ruS/oSBTREtBViDnlLXlvbHnrqHnkIbns7vnu58gYXZtb28gamF2YnVzIGphdmxpYnJhcnkg54is6JmrIOe6v+S4iiBBViDlvbHniYflm77kuabppoYgQVYg56OB5Yqb6ZO+5o6l5pWw5o2u5bqTVGhpbmtQSFAgRnJhbWV3b3JrIOWNgeW5tOWMoOW/g+eahOmrmOaAp+iDvVBIUOahhuaetiDmnIDlhajnmoTliY3nq6/otYTmupDmsYfmgLvku5PlupMg5YyF5ous5YmN56uv5a2m5LmgIOW8gOWPkei1hOa6kCDmsYLogYzpnaLor5XnrYkg5aSa6K+t6KiA5aSa6LSn5biB5aSa5YWl5Y+j55qE5byA5rqQ55S15ZWGIEIyQyDllYbln44g5pSv5oyB56e75Yqo56uvdnVlIGFwcCBodG1sNSDlvq7kv6HlsI/nqIvluo/lvq7lupcg5b6u5L+h5bCP56iL5bqP5ZWG5Z+O562J5Y+v6IO95piv5oiR55So6L+H55qE5pyA5LyY6ZuF55qEIEFsaXBheSDlkowgV2VDaGF0IOeahOaUr+S7mCBTREsg5omp5bGV5YyF5LqGIOWfuuS6juivjeW6k+eahOS4reaWh+i9rOaLvOmfs+S8mOi0qOino+WGs+aWueahiCDmiJHnlKjniKzomavkuIDlpKnml7bpl7TigJzlgbfkuobigJ3nn6XkuY7kuIDnmb7kuIfnlKjmiLcg5Y+q5Li66K+B5piOUEhQ5piv5LiW55WM5LiK5pyA5aW955qE6K+t6KiAIOaJgOS9v+eUqOeahOeoi+W6j+W+ruS/oSBTREsgZm9yIExhcmF2ZWwg5Z+65LqOIG92ZXJ0cnVlIHdlY2hhdOW8gOa6kOWcqOe6v+aVmeiCsueCueaSreezu+e7nyAg5LiA5qy+5ruh6Laz5L2g55qE5aSa56eN5Y+R6YCB6ZyA5rGC55qE55+t5L+h5Y+R6YCB57uE5Lu2IOWfuuS6jiBMYXJhdmVsIOeahOWQjuWPsOezu+e7n+aehOW7uuW3peWFtyBMYXJhdmVsIEFkbWluIOS9v+eUqOW+iOWwkeeahOS7o+eggeW/q+mAn+aehOW7uuS4gOS4quWKn+iDveWujOWWhOeahOmrmOminOWAvOWQjuWPsOezu+e7nyDlhoXnva7kuLDlr4znmoTlkI7lj7DluLjnlKjnu4Tku7Yg5byA566x5Y2z55SoIOiuqeW8gOWPkeiAheWRiuWIq+WGl+adgueahEhUTUzku6PnoIHkuIDkuKrmg7PluK7kvaDmgLvnu5PmiYDmnInnsbvlnovnmoTkuIrkvKDmvI/mtJ7nmoTpnbblnLrkvJjpm4XnmoTmuJDov5vlvI9QSFDph4fpm4bmoYbmnrYgTGFyYXZlbCDnlLXllYblrp7miJjmlZnnqIvnmoTpobnnm67ku6PnoIFQYXltZW505pivcGhw54mI5pys55qE5pSv5LuY6IGa5ZCI56ys5LiJ5pa5c2RrIOmbhuaIkOS6huW+ruS/oeaUr+S7mCDmlK/ku5jlrp3mlK/ku5gg5oub5ZWG5LiA572R6YCa5pSv5LuYIOaPkOS+m+e7n+S4gOeahOiwg+eUqOaOpeWPoyDmlrnkvr/lv6vpgJ/mjqXlhaXlkITnp43mlK/ku5gg5p+l6K+iIOmAgOasviDovazotKbog73lipsg5pyN5Yqh56uv5o6l5YWl5pSv5LuY5Yqf6IO9IOaWueS+vyDlv6vmjbcgU1BGIFN3b29sZSBQSFAgRnJhbWV3b3JrIOS4lueVjOesrOS4gOasvuWfuuS6jlN3b29sZeaJqeWxleeahFBIUOahhuaetiDlvIDlj5HogIXmmK9Td29vbGXliJvlp4vkurogQSBXb25kZXJmdWwgV29yZFByZXNzIFRoZW1lIOaoseiKseW6hOeahOeZveeMq+WNmuWuouS4u+mimOWbvuW6iiDmraTpobnnm67lt7LlvIPnlKgg5Z+65LqOIFRoaW5rUEhQIOWfuuehgOW8gOWPkeW5s+WPsCDnmbvlvZXotKblj7flr4bnoIHpg73mmK8gYWRtaW4gUGFuRG93bmxvYWTnvZHpobXlpI3liLvniYjkuIDkuKrlvIDmupDnmoTnvZHlnYDlr7zoiKrnvZHnq5npobnnm64g5oKo5Y+v5Lul5ou/5p2l5Yi25L2c6Ieq5bex55qE572R5Z2A5a+86IiqIOS9v+eUqFBIUCBTd29vbGXlrp7njrDnmoTnvZHpobXljbPml7bogYrlpKnlt6Xlhbcg54us6KeS5pWw5Y2hIOWPkeWNoSDlvIDmupDlvI/nq5nplb/oh6rliqjljJbllK7otKfop6PlhrPmlrnmoYgg6auY5pWIIOeos+WumiDlv6vpgJ8g5Y2h5a+G5ZWG5Z+O57O757ufIOmrmOaViOWuieWFqOeahOWcqOe6v+WNoeWvhuWVhuWfjiDvuI/lkb3ku6TooYzmqKHlvI/lvIDlj5HmoYbmnrZTaG9wWE/lhY3otLnlvIDmupDllYbln47ns7vnu58g5Zu95YaF6aKG5YWI5LyB5Lia57qnQjJD5YWN6LS55byA5rqQ55S15ZWG57O757ufIOWMheWQq1BDIGg1IOW+ruS/oeWwj+eoi+W6jyDmlK/ku5jlrp3lsI/nqIvluo8g55m+5bqm5bCP56iL5bqPIOWktOadoSDmipbpn7PlsI/nqIvluo8gUVHlsI/nqIvluo8gQVBQIOWkmuWVhuaItyDpgbXlvqpNSVTlvIDmupDljY/orq7lj5HluIMg5Z+65LqOIFRoaW5rUEhQNSAx5qGG5p6256CU5Y+RV2l6YXJk5piv5LiA5qy+5byA5rqQ55qE5paH5qGj566h55CG5bel5YW3IOaUr+aMgU1hcmtkb3duIFN3YWdnZXIgVGFibGXnsbvlnovnmoTmlofmoaMgU3dvb2xlIE15U1FMIFByb3h5IOS4gOS4quWfuuS6jiBNeVNRTCDljY/orq4gU3dvb2xlIOW8gOWPkeeahE15U1FM5pWw5o2u5bqT6L+e5o6l5rGgIOWtpuS5oOi1hOa6kOaVtOWQiEZyZWVub23ln5/lkI3oh6rliqjnu63mnJ/kuIDkuKrlpb3njqnnmoRXZWLlronlhagg5ryP5rSe5rWL6K+V5bmz5Y+w5LiA5Liq5Z+65LqOWWlpMumrmOe6p+ahhuaetueahOW/q+mAn+W8gOWPkeW6lOeUqOW8leaTjuiTneWkqemHh+mbhuWZqOaYr+S4gOasvuWFjei0ueeahOaVsOaNrumHh+mbhuWPkeW4g+eIrOiZq+i9r+S7tiDph4fnlKhwaHAgbXlzcWzlvIDlj5Eg5Y+v6YOo572y5Zyo5LqR5pyN5Yqh5ZmoIOWHoOS5juiDvemHh+mbhuaJgOacieexu+Wei+eahOe9kemhtSDml6DnvJ3lr7nmjqXlkITnsbtDTVPlu7rnq5nnqIvluo8g5YWN55m75b2V5a6e5pe25Y+R5biD5pWw5o2uIOWFqOiHquWKqOaXoOmcgOS6uuW3peW5sumihCDmmK/nvZHpobXlpKfmlbDmja7ph4fpm4bova/ku7bkuK3lrozlhajot6jlubPlj7DnmoTkupHnq6/niKzomavns7vnu5/lhY3otLnlvIDmupDnmoTkuK3mlofmkJzntKLlvJXmk44g6YeH55SoIEMgQyDnvJblhpkg5Z+65LqOIHhhcGlhbiDlkowgc2N3cyDmj5DkvpsgUEhQIOeahOW8gOWPkeaOpeWPo+WSjOS4sOWvjOaWh+aho1dEU2Nhbm5lcuW5s+WPsOebruWJjeWunueOsOS6huWmguS4i+WKn+iDvSDliIbluIPlvI93ZWLmvI/mtJ7miavmj48g5a6i5oi3566h55CGIOa8j+a0nuWumuacn+aJq+aPjyDlrZDln5/lkI3mnprkuL4g56uv5Y+j5omr5o+PIOe9keermeeIrOiZqyDmmpfpk77mo4DmtYsg5Z2P6ZO+5qOA5rWLIOe9keermeaMh+e6ueaQnOmbhiDkuJPpobnmvI/mtJ7mo4DmtYsg5Luj55CG5pCc6ZuG5Y+K6YOo572y562J5Yqf6IO9ICDvuI/lhbDnqbrlm77luorlm77moIflt6XlnLog56e75Yqo5bqU55So5Zu+5qCH55Sf5oiQ5bel5YW3IOS4gOmUrueUn+aIkOaJgOacieWwuuWvuOeahOW6lOeUqOWbvuagh+WSjOWQr+WKqOWbviBBcmdvbiDkuIDkuKrovbvnm4gg566A5rSB55qEIFdvcmRQcmVzcyDkuLvpophUeXBlY2hvIEZhbnPmj5Lku7bkvZzlk4Hnm67lvZVQSFDku6PnoIHlrqHorqHliIbmrrXorrLop6PkuIDkuKrnu5PmnoTmuIXmmbDnmoQg5piT5LqO57u05oqk55qEIOeOsOS7o+eahFBIUCBNYXJrZG93buino+aekOWZqOeZvuW6pui0tOWQp+S6keetvuWIsCDlnKjmnI3liqHlmajkuIrphY3nva7lpb3lsLHml6DpnIDov5vooYzku7vkvZXmk43kvZzkvr/lj6/ku6Xlrp7njrDotLTlkKfnmoTlhajoh6rliqjnrb7liLAg6YWN5ZCI5o+S5Lu25L2/55So6L+Y5Y+v5a6e546w5LqR54GM5rC0IOeCuei1niDlsIHnpoEg5Yig5biWIOWuoeafpeetieWKn+iDvSDms6jmhI8gR2l0ZWUg5Y6fR2l0IG9zYyDku5PlupPlsIbkuI3lho3nu7TmiqQg55uu5YmN5ZSv5LiA5oyH5a6a55qE5LuT5bqT5Li6IEdpdGh1YiDmnKzpobnnm67msqHmnInlrpjmlrnkuqTmtYHnvqQg5aaC6ZyA5Lqk5rWB5Y+v5Lul55u05o6l5L2/55SoR2l0aHVi55qERGlzY3Vzc2lvbnMg5rKh5pyJ5ZWG5Lia54mI5pysIOebruWJjei0tOWQp+S6keetvuWIsOeUseekvuWMuuWFseWQjOe7tOaKpCDkuI3kvJrlgZzmraLmm7TmlrAgUFIg6YCa5bi45Zyo5LiA5aSp5YaF5aSE55CGIOW+ruS/oeiwg+ivlSBBUEnosIPor5XlkoxBSkFY55qE6LCD6K+V55qE5bel5YW3IOiDveWwhuaXpeW/l+mAmui/h1dlYlNvY2tldOi+k+WHuuWIsENocm9tZea1j+iniOWZqOeahGNvbnNvbGXkuK0g57WQ5be0IOS4reaWh+WIhuipniDlgZrmnIDlpb3nmoQgUEhQIOS4reaWh+WIhuipniDkuK3mlofmlrfoqZ7ntYTku7ZFbGVUZWFt5byA5rqQ6aG555uuIOeUteWVhuWFqOWll+ino+WGs+aWueahiOS5i1BIUOeJiCBTaG9wIGZvciBQSFAgWWlpMiDkuIDkuKrnsbvkvLzkuqzkuJwg5aSp54yrIOa3mOWuneeahOWVhuWfjiDmnInlr7nlupTnmoRBUFDmlK/mjIEg55SxRWxlVGVhbeWboumYn+e7tOaKpCBSaGFQSFDmmK/lvq7kv6HnrKzkuInmlrnnrqHnkIblubPlj7Ag5b6u5L+h5YWs5LyX5Y+3566h55CG57O757ufIOaUr+aMgeWkmuWFrOS8l+WPt+euoeeQhiBDUk3kvJrlkZjnrqHnkIYg5bCP56iL5bqP5byA5Y+RIEFQUOaOpeWPo+W8gOWPkSDlh6DkuY7pm4blkIjlvq7kv6Hlip/og70g566A5rSBIOW/q+mAn+S4iuaJiyDlv6vpgJ/lvIDlj5Hlvq7kv6HlkITnp43lkITmoLflupTnlKgg566A5rSBIOWlveeUqCDlv6vpgJ8g6aG555uu5byA5Y+R5b+r5Yeg5YCNIOe+pCA2NTY4Njgg5LiA5Yi756S+5Yy65ZCO56uvIEFQSSDmupDnoIEg5pawIOW+ruS/oeacjeWKoeWPtyDlvq7kv6HlsI/nqIvluo8g5b6u5L+h5pSv5LuYIOaUr+S7mOWuneaUr+S7mOiLueaenGNtcyB2MSBtYWNjbXMgdjEg6bqm5YWLY21zIOW8gOa6kGNtcyDlhoXlrrnnrqHnkIbns7vnu58g6KeG6aKR5YiG5Lqr56iL5bqPIOWIhumbhuWJp+aDheeoi+W6jyDnvZHlnYDlr7zoiKrnqIvluo8g5paH56ug56iL5bqPIOa8q+eUu+eoi+W6jyDlm77niYfnqIvluo/kuIDkuKpQSFDmlofku7bmkJ7lrprmlK/ku5jlrp3mlK/ku5jns7vliJcg5YyF5ous55S16ISR572R56uZ5pSv5LuYIOaJi+acuue9keermeaUr+S7mCDnjrDph5HnuqLljIUg5raI6LS557qi5YyFIOaJq+eggeaUr+S7mCBKU0FQSeaUr+S7mCDljZXnrJTovazotKbliLDmlK/ku5jlrp3otKbmiLcg5Lqk5piT57uT566XIOWIhui0piDliIbmtqYg572R6aG15o6I5p2D6I635Y+W55So5oi35L+h5oGv562JcmVzdGZ1bCBhcGnpo47moLzmjqXlj6MgQVBQ5o6l5Y+jIEFQUOaOpeWPo+adg+mZkCBvYXV0aDIg5o6l5Y+j54mI5pys566h55CGIOaOpeWPo+mJtOadg+WfuuS6juS8geS4muW+ruS/oeeahOW8gOa6kFNDUk3lupTnlKjlvIDlj5HmoYbmnrYg5byV5pOOIOS5n+aYr+S4gOWll+mAmueUqOeahOS8geS4muengeWfn+a1gemHj+euoeeQhuezu+e7nyBBUEnmjqXlj6PlpKflhajkuI3mlq3mm7TmlrDkuK0g5qyi6L+ORm9ya+WSjFN0YXIgMSDkuIDoqIAg5Y+k6K+X5Y+l54mIIGFwaSAyIOW/heW6lOavj+aXpeS4gOWbvmFwaSAzIOWcqOe6v2lw5p+l6K+iIDQgbTN1OOinhumikeWcqOe6v+ino+aekGFwaSA1IOmaj+acuueUn+aIkOS6jOasoeWFg+WbvueJh2FwaSA2IOW/q+mAkuafpeivomFwaSDmlK/mjIHlm73lhoXnmb7lrrblv6vpgJIgNyBmbHbop4bpopHlnKjnur/op6PmnpBhcGkgOCDmipbpn7Pop4bpopHml6DmsLTljbDop6PmnpBhcGkgOSDkuIDlj6Xor53pmo/mnLrlm77niYdhcGkgMSBRUeeUqOaIt+S/oeaBr+iOt+WPlmFwaSAxMSDlk5Tlk6nlk5Tlk6nlsIHpnaLlm77ojrflj5ZhcGkgMTIg5Y2D5Zu+572RNThwaWPml6DmsLTljbDop6PmnpDkuIvovb1hcGkgMTMg5Zac6ams5ouJ6ZuF5Li75pKtRk3mlbDmja7ph4fpm4ZhcGkgMTQg572R5piT5LqR6Z+z5LmQYXBpIDE1IENDVFblpK7op4bnvZHop4bpopHop6PmnpBhcGkgMTYg5b6u5L+h6L+Q5Yqo5Yi35q2l5pWwYXBpIDE3IOearuearuaQnueskSDln7rkuo5zd29vbGXnmoTlrprml7blmajnqIvluo8g5pSv5oyB56eS57qn5aSE55CG576kIDY1Njg2OCDvuI8gU2FiZXIgUEhQ5byC5q2l5Y2P56iLSFRUUOWuouaIt+err+W+ruS/oeaUr+S7mOWNleaWh+S7tueJiCDkuIDkuKpQSFDmlofku7bmkJ7lrprlvq7kv6HmlK/ku5jns7vliJcg5YyF5ous5Y6f55Sf5pSv5LuYIOaJq+eggeaUr+S7mCBINeaUr+S7mCDlhazkvJflj7fmlK/ku5gg546w6YeR57qi5YyFIOS8geS4muS7mOasvuWIsOmbtumSseetiSDmlrDlop5WM+eJiCDkuIDkuKrov5jkuI3plJnnmoTlm77luorlt6Xlhbcg5pSv5oyBTWFjIFdpbiBMaW51eOacjeWKoeWZqCDmlK/mjIHljovnvKnlkI7kuIrkvKAg5re75Yqg5Zu+54mH5oiW5paH5a2X5rC05Y2wIOWkmuaWh+S7tuWQjOaXtuS4iuS8oCDlkIzml7bkuIrkvKDliLDlpJrkuKrkupEg5Y+z5Ye75Lu75oSP5paH5Lu25LiK5LygIOW/q+aNt+mUruS4iuS8oOWJqui0tOadv+aIquWbviBXZWLniYjkuIrkvKAg5pSv5oyB5L2c5Li6TXdlYiBUeXBvcmHlj5HluIPlm77niYfmjqXlj6Mg5L2c5Li6UGljR28gU2hhcmVYIHVQaWPnrYnnmoToh6rlrprkuYnlm77luoog5pSv5oyB5Zyo5pyN5Yqh5Zmo5LiK6YOo572y5L2c5Li65Zu+5bqK5o6l5Y+jIOaUr+aMgeS4iuS8oOS7u+aEj+agvOW8j+aWh+S7tiDlj6/og73mmK/miJHnlKjov4fnmoTmnIDkvJjpm4XnmoQgQWxpcGF5IOWSjCBXZUNoYXQg55qEIGxhcmF2ZWwg5pSv5LuY5omp5bGV5YyF5LqG5LiK5Lyg5aSn5paH5Lu255qETGFyYXZlbOaJqeWxleWMheW8gOWPkeWGheWKn+S/rueCvExhcmF2ZWzmoLjlv4Pku6PnoIHlrabkuaDljZfkuqzpgq7nlLXlpKflrablvIDmupAgT25saW5lIEp1ZGdlIFFR576kIDY2ODEgODI2NCDlhY3otLlJUOWcsOWdgOaVsOaNruW6kyDlt7LmlK/mjIFJUFY0IElQVjYg57uT5p6E5YyW6L6T5Ye65Li65Zu95a62IOecgSDluIIg5Y6/IOi/kOiQpeWVhiDkuK3mlofmlbDmja7lupMg5pa55L6/5a6e55SoIGxhcmF2ZWw1IDXlkox2dWUganPnu5PlkIjnmoTliY3lkI7nq6/liIbnprvpobnnm67mqKHmnb8g5ZCO56uv5L2/55So5LqGbGFyYXZlbOeahExUU+eJiOacrCA1IDUg5YmN56uv5L2/55So5LqG5rWB6KGM55qEdnVlIGVsZW1lbnQgdGVtcGxhdGXpobnnm64g5L2c5Li656iL5bqP55qE6LW354K5IOWPr+S7peebtOaOpeS7peatpOS4uuWfuuehgOadpei/m+ihjOS4muWKoeaJqeWxlSDmqKHmnb/lhoXlrrnljIXmi6zln7rnoYDnmoTnlKjmiLfnrqHnkIblkozmnYPpmZDnrqHnkIYg5pel5b+X566h55CGIOmbhuaIkOesrOS4ieaWueeZu+W9lSDmlbTlkIhsYXJhdmVsIGVjaG8gc2VydmVyIOWunueOsOS6hndlYnNvY2tldCDlgZrliLDkuobmtojmga/nmoTlrp7ml7bmjqjpgIEg5bm25Zyo5q2k5Z+656GA5LiKIOWunueOsOS6huiBiuWkqeWupOWSjOWuouacjeWKn+iDvSDmnYPpmZDnrqHnkIbljIXmi6zlkI7nq69Ub2tlbuiupOivgeWSjOWJjeerr3Z1ZSBqc+eahOWKqOaAgeadg+mZkCDop6PlhrPkuobliY3lkI7nq6/lrozmlbTliIbnprvnmoTmg4XlhrXkuIsgdnVlIGpz55qE6K6k6K+B5LiO5p2D6ZmQ55u45YWz55qE55eb54K5IOW3suWcqOacrOS6uueahOWkmuS4qumhueebruS4rembhuaIkOS9v+eUqCAgV2Vi5a6J5YWo5LmL5py65Zmo5a2m5Lmg5YWl6ZeoIOe9keaYk+S6kemfs+S5kOWNh+e6p0FQSVBIUCDpm4bmiJDmlK/ku5ggU0RLIOmbhuaIkOS6huaUr+S7mOWunSDlvq7kv6HmlK/ku5jnmoTmlK/ku5jmjqXlj6PlkozlhbblroPnm7jlhbPmjqXlj6PnmoTmk43kvZwg5pSv5oyBIHBocCBmcG0g5ZKMIFN3b29sZSDmiYDmnInmoYbmnrbpgJrnlKgg5a6H5ramUEhQ5YWo5a625qG25oqA5pyv5pSv5oyB576kIDE3OTE2MjI3TURDbHViIOekvuWMuuezu+e7n+WQjuerr+S7o+eggWltaSDmmK/ln7rkuo4gU3dvb2xlIOeahCBQSFAg5Y2P56iL5byA5Y+R5qGG5p62IOWug+aUr+aMgSBIdHRwMiBXZWJTb2NrZXQgVENQIFVEUCBNUVRUIOetieS4u+a1geWNj+iurueahOacjeWKoeW8gOWPkSDnibnliKvpgILlkIjkupLogZTnvZHlvq7mnI3liqEg5Y2z5pe26YCa6K6v6IGK5aSpaW0g54mp6IGU572R562J5Zy65pmvIFFR576kIDE3OTE2MjI3V29yZFByZXNzIOeJiCBXZWJTdGFjayDlr7zoiKrkuLvpopggbmF2IGlvd2VuIGNuTGl2ZTJEIOeci+adv+WomOaPkuS7tiB3d3cgZmdocnNoIG5ldCBwb3N0IDEyMyBodG1sIOS4iuS9v+eUqOeahOWQjuerryBBUEnnroDljZXmkJzntKIg5LiA5Liq566A5Y2V55qE5YmN56uv55WM6Z2iIOeUqOaDr+S6huWQhOenjeWvvOiIqummlumhtSDmu6HlsY/luZXlsL3mmK/lkITnp43kuI3ljozlhbbng6bnmoTlub/lkYrlkozotYTorq8g5bCd6K+V6Ieq5bex5YaZ5Liq6Ieq5bex55qE5Li76aG1IOWbveWGheWQhOWkp0NURui1m+mimOWPindyaXRldXDmlbTnkIbmlLbpm4boh6rnvZHnu5zlkITlpITnmoQgd2Vic2hlbGwg5qC35pysIOeUqOS6jua1i+ivlSB3ZWJzaGVsbCDmiavmj4/lmajmo4DmtYvnjocgIFBIUOW+ruS/oVNESyDlvq7kv6HlubPlj7Ag5b6u5L+h5pSv5LuYIOeggeWwj+WFrSBHaXRIdWIg5Luj56CB5rOE6Zyy55uR5o6n57O757ufUEhQ6KGo5Y2V55Sf5oiQ5ZmoIOW/q+mAn+eUn+aIkOeOsOS7o+WMlueahGZvcm3ooajljZUg5pSv5oyB5YmN5ZCO56uv5YiG56a7IOWGhee9ruWkjemAieahhiDljZXpgInmoYYg6L6T5YWl5qGGIOS4i+aLiemAieaLqeahhiDnnIHluILljLrkuInnuqfogZTliqgg5pe26Ze06YCJ5oupIOaXpeacn+mAieaLqSDpopzoibLpgInmi6kg5paH5Lu2IOWbvueJh+S4iuS8oOetiTE356eN5bi455So57uE5Lu2IOaCn+epukNSTSDln7rkuo5UUDUgdnVlIEVsZW1lbnRVSeeahOWJjeWQjuerr+WIhuemu0NSTeezu+e7n1blhY3nrb5QSFDniYgg5a6M5YWo5byA5rqQ5YWN6LS555qE5Liq5Lq65YWN562+57qm6Kej5Yaz5pa55qGIQ29tcG9zZXIg5YWo6YeP6ZWc5YOP5Y+R5biD5LqOMiAxN+W5tDPmnIgg5pu+5LiN6Ze05pat6L+Q6KGMMuW5tOWkmiDov5nkuKrlvIDmupDmnInliqnkuo7nkIbop6MgQ29tcG9zZXIg6ZWc5YOP55qE5bel5L2c5Y6f55CG5LiA5Liq5aSa5b2pIOi9u+advuS4iuaJiyDkvZPpqozlrozlloQg5YW35pyJ5by65aSn6Ieq5a6a5LmJ5Yqf6IO955qEV29yZFByZXNz5Li76aKYIOWfuuS6jlNha3VyYeS4u+mimOWFqOeQg+WFjei0ueS7o+eQhklQ5bqTIOmrmOWPr+eUqElQIOeyvuW/g+etm+mAieS8mOi0qElQIDJz5b+F6L6+TGFyYUNNUyDmmK/lnKjlrabkuaAgbGFyYXZlbCB3ZWIg5byA5Y+R5a6e5oiY6L+b6Zi2IOWunuaImOaehOaetiBBUEkg5pyN5Yqh5ZmoIOi/h+eoi+S4reS6p+eUn+eahOS4gOS4quS4muS9meS9nOWTgSDor5Xlm77pgJrov4fnroDljZXnmoTmlrnlvI8g5b+r6YCf5p6E5bu65LiA5aWX5Z+65pys55qE5LyB5Lia56uZ5ZCM5pe25L+d55WZ5b6I54G15rS755qE5omp5bGV6IO95Yqb5ZKM5LyY6ZuF55qE5Luj56CB5pa55byPIOW9k+eEtui/meS6m+mDveW+l+ebikxhcmF2ZWznmoTkvJjnp4Dorr7orqEg5ZCM5pe2TGFyYUNNUyDkuZ/mmK/kuIDkuKrlrabkuaBMYXJhdmVsIOS4jemUmeeahOWPguiAg+ekuuS+iyAg5bey5YGc5q2i57u05oqkIEhvb2tQSFDln7rkuo5D5omp5bGV5pCt5bu65YaF572uQUnnvJbnqIvnmoTmnrbmnoTns7vnu58g5pSv5oyB5b6u5pyN5Yqh6YOo572yIOeDreaPkuaLlOS4muWKoee7hOS7tiDpm4bmiJDkuJrliqHmqKHlnosg5p2D6ZmQ5qih5Z6LIFVJ57uE5Lu25bqTIOWkmuaooeadvyDlpJrlubPlj7Ag5aSa5Z+f5ZCNIOWkmue7iOerryDlpJror63oqIAg5ZCr5bi46am75YaF5a2YIOWJjeWQjuWIhuemuyBBUEnlubPlj7AgTFVBIFFR576kIDY3OTExNjM4IOS4reWNjuS6uuawkeWFseWSjOWbveWxheawkei6q+S7veivgSDkuK3ljY7kurrmsJHlhbHlkozlm73muK/mvrPlsYXmsJHlsYXkvY/or4Hku6Xlj4rkuK3ljY7kurrmsJHlhbHlkozlm73lj7Dmub7lsYXmsJHlsYXkvY/or4Hlj7fnoIHpqozor4Hlt6XlhbcgUEhQIOeJiCDmnIDnroDljZXnmoQ5MXBvcm7niKzomatwaHDniYjmnKxGZW5kIOaYr+S4gOasvuefreWwj+eyvuaCjSDlj6/lnKggRlBNIFN3b29sZSDmnI3liqHlrrnlmajlubPmu5HliIfmjaLnmoTpq5jmgKfog71QSFDmoYbmnrYgbm8gZXZpbCDlrp7njrDov4fmu6TmlY/mhJ/or43msYcg5Z+65LqO56Gu5a6a5pyJ56m36Ieq5Yqo5py6IERGQSDnrpfms5Ug5pSv5oyBY29tcG9zZXLlronoo4XmianlsZVaIEJsb2dQSFDljZrlrqLnqIvluo9JWVVV6Ieq5Yqo6L6F56eN5bel5YW3IOebruWJjeiDveWvueWbveWGheWkp+mDqOWIhueahFBU56uZ54K56Ieq5Yqo6L6F56eNIOaUr+aMgeS4i+i9veWZqOmbhue+pCDmlK/mjIHlpJrnm5jkvY0g5pSv5oyB5aSa5LiL6L2955uu5b2VIOaUr+aMgei/nOeoi+i/nuaOpeetiSDmnpzphbHlsI/lupcg5Z+65LqOIExhcmF2ZWwgc3dvb2xlIOWwj+eoi+W6j+eahOW8gOa6kOeUteWVhuezu+e7nyDkvJjpm4XkuI7mgKfog73lhbzpob4g6YCZ5piv5LiA5Lu957SU6Z2g5YyX5bel56iL5bir55qE5bCI5qGIIOiri+WlveWlveaEm+itt+WugyDorJ3orJ0gRUMgZWNqaWEg5Yiw5a625piv5LiA5qy+5Y+v5byA5bGVTzJP5Lia5Yqh55qE56e75Yqo55S15ZWG57O757ufIOWug+WMheWQqyDnp7vliqjnq69BUFAg6YeH55So5Y6f55Sf5qih5byP5byA5Y+RIOimhuebluS9v+eUqGlPUyDlj4pBbmRyb2lk57O757uf55qE56e7IOWKqOe7iOerryDlkI7lj7Dns7vnu58g6ZKI5a+55bmz5Y+w5pel5bi46L+Q6JCl57u05oqk55qE5bmz5Y+w5ZCO5Y+wIOmSiOWvueWFpempu+W6l+mTuueuoeeQhueahOWVhuWutuWQjuWPsCDni6znq4vlubbooYwg56e75Yqo56uvSDUg6IO95aSf54G15rS76YOo572y5LqO5b6u5L+h5Y+K5YW25LuWQVBQIOe9kemhteetiSBNYXRlcmlhbCBEZXNpZ24g5oyH5Y2X55qE5Lit5paH57+76K+RIOS4gOS4que6r3BocOWIhuivjSB0aGlua3BocDUgMSBsYXl1aSDlrp7njrDnmoTluKZyYmFj55qE5Z+656GA566h55CG5ZCO5Y+wIOaWueS+v+W/q+mAn+W8gOWPkeazleS9v+eUqOeZvuW6pnBjc+S4iuS8oOiEmuacrOebruWJjeacgOWFqOeahOWJjeerr+W8gOWPkemdouivlemimOWPiuetlOahiOaoseiKseWGhee9keepv+mAj+e9keermea6kOS7o+eggSAyIDIg6YeN5Yi254mITWVlcG9QU+aYr01lZXBvIFBIUCBTb2NrZXTnmoTnvKnlhpkg5peo5Zyo5o+Q5L6b56iz5a6a55qEU29ja2V05pyN5YqhIOWPr+S7pei9u+advuaehOW7uuWcqOe6v+WunuaXtuiBiuWkqSDljbPml7bmuLjmiI8g6KeG6aKR5rWB5aqS5L2T5pKt5pS+562JIOWfuuehgOebruW9lSDogZrlkIjmiYDmnInlhbbku5bnm67lvZUg5YyF5ZCr5paH5qGj5ZKM5L6L5a2Q5Z+65LqOIFZ1ZSBqcyDnmoTnroDmtIHkuIDoiKzlvLrlpKfnmoQgV29yZFByZXNzIOWNleagj+WNmuWuouS4u+mimOmYv+mHjOS6keaJk+mAoExhcmF2ZWzmnIDlpb3nmoRPU1MgU3RvcmFnZeaJqeWxlSDnvZHkuIrlnKjnur/llYbln44g57u85ZCI572R5LiK6LSt54mp5bmz5Y+wc3dvb2xlZnnmmK/kuIDkuKrln7rkuo5zd29vbGXlrp7njrDnmoTovbvph4/nuqcg6auY5oCn6IO9IOWNj+eoi+e6pyDlvIDmlL7mgKfnmoRBUEnlupTnlKjmnI3liqHmoYbmnrbln7rkuo5yZWRpc+WunueOsOmrmOWPr+eUqCDmmJPmi5PlsZUg5o6l5YWl5pa55L6/IOeUn+S6p+eOr+Wig+eos+Wumui/kOihjOeahOW7tui/n+mYn+WIlyDkuIDmrL7ln7rkuo5Xb3JkUHJlc3PlvIDlj5HnmoTpq5jpopzlgLznmoToh6rpgILlupTkuLvpopgg5pSv5oyB55m95aSp5LiO6buR5aSc5qih5byPIOaXoOWIt+aWsOWKoOi9veetiSDpmL/ph4zkupEgT1NTIOWumOaWuSBTREsg55qEIENvbXBvc2VyIOWwgeijhSDmlK/mjIHku7vkvZUgUEhQIOmhueebriDljIXmi6wgTGFyYXZlbCBTeW1mb255IFRpbnlMYXJhIOetieetiSDmraTmj5Lku7blsIbkvaDnmoRXb3JkUHJlc3PmjqXlhaXmnKzlnJ/nlJ/mgIHkvZPns7vkuYvkuK0g5L2/5LmL5pu06YCC5ZCI5Zu95YaF5bqU55So546v5aKDUEhQ55qE5pyN5Yqh5YyW5qGG5p62IOmAgueUqOS6jkFwaSBTZXJ2ZXIgUnBjIFNlcnZlciDluK7liqnljp/nlJ9QSFDpobnnm67ovazlkJHlvq7mnI3liqHljJYg5Ye66Imy55qE5oCn6IO95LiO5pSv5oyB6auY5bm25Y+R55qE5Y2P56iL55u457uT5ZCI5Z+65LqOVGhpbmtQSFAgVjYg5byA5Y+R55qE6Z2i5ZCRQVBJ55qE5ZCO5Y+w566h55CG57O757ufICBQSFAgU3dvb2xlIOW8gOWPkeeahOWcqOe6v+WQjOatpeeCueatjOWPsCDmlK/mjIHoh6rnlLHngrnmrYwg5YiH5q2MIOiwg+aVtOaOkuW6jyDliKDpmaTmjIflrprpn7PkuZDku6Xlj4rln7rnoYDmnYPpmZDliIbnuqfkv6Hlkbwg5YWN6LS55byA5rqQ55qE5Yqe5YWsT0Hns7vnu58g5YyF5ousQVBQIHBj5LiK5a6i5oi356uvIFJFSU3ljbPml7bpgJrkv6Eg5pyN5Yqh56uv562JIOiuqeavj+S4quS8geS4muWNleS9jemDveacieiHquW3seeahOWKnuWFrOezu+e7nyDmnaXlrqLnlLXllYYg5b6u5L+h5bCP56iL5bqP5ZWG5Z+OIEFQUOWVhuWfjiDlhazkvJflj7fllYbln44gUEPllYbln47ns7vnu58g5pSv5LuY5a6d5bCP56iL5bqP5ZWG5Z+OIOaKlumfs+Wwj+eoi+W6j+WVhuWfjiDnmb7luqblsI/nqIvluo/nlLXllYbns7vnu58g5YmN5ZCO56uv5Luj56CB5YWo6YOo5byA5rqQIOazqOmHjeeVjOmdoue+juaEn+S4jueUqOaIt+S9k+mqjCDmiZPpgKDni6znibnnlLXllYbns7vnu5/nlJ/mgIHlnIjlk5Tlk6nlk5Tlk6kgQmlsaWJpbGkgQiDnq5nkuLvnq5nliqnmiYsg55u05pKt5Yqp5omLIOebtOaSreaKveWlliDmjILmnLrljYfnuqcg6LS05b+D5bCP5qOJ6KKE6ISa5pysIEx2NiDnprvkvaDku4XmnInkuIDmraXkuYvpgaUgUEhQIOeJiCBQZXJzb25hbCDkuIDkuKrov5DnlKhwaHDkuI5zd29vbGXlrp7njrDnmoTnu5/orqHnm5Hmjqfns7vnu5/nn63op4bpopHljrvmsLTljbAg5oqW6Z+zIOearuearuiZviDngavlsbEg5b6u6KeGIOW+ruWNmiDnu7/mtLIg5pyA5Y+zIOi9u+inhumikSDlv6vmiYsg5YWo5rCR5bCP6KeG6aKRIOW3tOWhnueUteW9sSDpmYzpmYwgQmVmb3Jl6YG/6aOOIOW8gOecvCBWdWUgVmxvZyDlsI/lkpbnp4Ag55qu55qu5pCe56yRIOWFqOawkUvmrYwg6KW/55Oc6KeG6aKRICDkuK3lm73lhpzljoYg6Zi05Y6GIOS4jumYs+WOhiDlhazljoYg6L2s5o2i5LiO5p+l6K+i5bel5YW3QW9pQVdEIOS4k+S4uuavlOi1m+iuvuiuoSDkvr/mkLrmgKflpb0g5L2O5p2D6ZmQ6L+Q6KGM55qERURS57O757ufIOmhueebrueuoeeQhuezu+e7n+WQjuerr+aOpeWPo1RoaW5rUEhQIOmYn+WIl+aUr+aMgVR5cGVjaG8gVGhlbWUgQXJpYSDkuablhpnoh6rlt7HnmoTnr4fnq6BQSFAg5Lit5paH5bel5YW35YyFIOaUr+aMgeaxieWtl+i9rOaLvOmfsyDmi7zpn7PliIbor40g566A57mB5LqS6L2sIOaVsOWtlyDph5Hpop3lpKflhpkgUVHnvqQgMTc5MTYyMjdZaWkyIGNvbW11bml0eSDor7forr/pl67mt5jlrqI15ZCI5LiAU0RLIOaUr+aMgea3mOWuneiBlOebnyDkuqzkuJzogZTnm58g5aSa5aSa6L+b5a6dIOWUr+WTgeS8miDoi4/lroHln7rkuo4gdGhpbmtwaHAg5byA5Y+R55qE55qEIGJsb2dNb2ppdG8gQWRtaW4g5Z+65LqOIExhcmF2ZWwgVnVlIEVsZW1lbnQg5p6E5bu655qE5ZCO5Y+w566h55CG57O757uf5LiA5Liq57uP5YW455qEWFNT5riX6YCP566h55CG5bmz5Y+w5LiA5qy+5Z+65LqOIFJhZ2VGcmFtZTIg55qE5YWN6LS55byA5rqQ55qE5Z+656GA6ZSA5ZSu5Yqf6IO955qE5ZWG5Z+O5Z+65LqOTGFyYXZlbCA1IDQg55qE5byA5Y+R55qE5Y2a5a6i57O757ufIOS7o+WPtyBteVBlcnNpbW1vbuivgeS7tueFp+eJh+aOkueJiOWcqOe6v+eUn+aIkOWZqCDlnKjkuIDlvKA25a+455qE54Wn54mH5LiK5o6S54mI5aSa5byg6K+B5Lu254Wn5riF5Y2O5aSn5a2m6K6h566X5py65a2m56eR5o6o6I2Q5a2m5pyv5Lya6K6u5ZKM5pyf5YiK5YiX6KGoV29yZFByZXNz5ZON5bqU5byP5YWN6LS55Li76aKYIEFydCBCbG9n5ZSv5ZOB56eA5Y2a5a6iIHdlaXB4aXUgY29tIOWkh+eUqOWfn+WQjXdlaXB4aXUgY24g5byA5rqQ57uZ5bCP5LyZ5Ly05YWN6LS55L2/55SoIOWmguS9v+eUqOi/h+eoi+acieS7u+S9lemXrumimCDlnKjnur/mioDmnK/mlK/mjIFRUSDmrKLov47miZPmibAg5Y6f5Yib5LiN5piTIOWmguWWnOasoiDor7flpJrlpJrmiZPotY8g5ryU56S6IEV3b01haWzmmK/ln7rkuo5MaW51eOeahOS8geS4mumCrueuseacjeWKoeWZqCDpm4bmiJDkuobkvJflpJrkvJjnp4DnqLPlrprnmoTnu4Tku7Yg5piv5LiA5Liq5b+r6YCf6YOo572yIOeugOWNlemrmOaViCDlpJror63oqIAg5a6J5YWo56iz5a6a55qE6YKu5Lu26Kej5Yaz5pa55qGIIOeslOiusOacrOaWsOeJiOeugOWNleW8uuWkp+eahOaXoOaVsOaNruW6k+eahOWbvuW6ijIg54mIIOa8lOekuuWcsOWdgCAgQmlsaWJpbGkgQiDnq5noh6rliqjpoobnk5zlrZAg55u05pKt5Yqp5omLIOebtOaSreaMguacuuiEmuacrCDkuLvnq5nliqnmiYsgUEhQIOeJiOW+ruS/oee+pOS6jOe7tOeggea0u+eggeW3peWFtyDnlJ/miJDlvq7kv6HnvqTmtLvnoIEg6ZqP5pe25Y+v5Lul5YiH5o2i5LqM57u056CBIOefreinhumikeeahFBIUOaLk+WxleWMhSDpm4bmiJDlkITlpKfnn63op4bpopHnmoTljrvmsLTljbDlip/og70g5oqW6Z+zIOW/q+aJiyDlvq7op4bkuLvmtYHnn63op4bpopEgUEhQ5Y675rC05Y2w5LiA5LiqUEhQZXLnmoTljYfnuqfkuYvot6/phbfnk5zkupHor77loIIg5Zyo57q/5pWZ6IKyIOe9keivvuezu+e7nyDnvZHmoKHns7vnu58g55+l6K+G5LuY6LS557O757ufIOS4jeWKoOWvhuS4jemYieWJsiAxICXlhajlip/og73lvIDmupAg5Y+v5YWN6LS55ZWG55SoIOahhuaetuS4u+imgeS9v+eUqFRoaW5rUEhQNiBsYXl1aSDmi6XmnInlrozlloTnmoTmnYPpmZDnmoTnrqHnkIbmqKHlnZfku6Xlj4rmlY/mjbfnmoTlvIDlj5HmlrnlvI8g6K6p5L2g5byA5Y+R6LW35p2l5pu05Yqg55qE6IiS5pyNIGxhcmF2ZWw1IDXmkK3lu7rnmoTlkI7lj7DnrqHnkIYg5ZKMIGFwaeacjeWKoSDnmoTlsI/nqIvluo/llYbln47ln7rkuo5UaGlua1BIUDUgQWRtaW5MVEXnmoTlkI7lj7DnrqHnkIbns7vnu5/prZTmlLnniYjmnKwg5Li6IE9MQUlOREVYIOa3u+WKoOWkmue9keebmOaMgui9veWPiuS4gOS6m+Wwj+S/ruWkjea1t+ixmlBIUCDln7rkuo5UaGlua1BIUDUgMSA0MUxUU+eahOW/q+mAn+W8gOWPkeahhuaetuaMgui9vVRlYW1iaXRpb27mlofku7Yg5Y+v55u06ZO+5YiG5LqrIOaUr+aMgee9keebmCDpnIDnlLPor7cg5ZKM6aG555uu5paH5Lu2IOaXoOmcgOmCgOivt+eggSDlh4bnoa7njoc5OSA5JeeahGlw5Zyw5Z2A5a6a5L2N5bqTbGFyYXZlbCBhbnQgZGVzaWduIHZ1ZSDmnYPpmZDlkI7lj7BQSFAg56ys5LiJ5pa555m75b2V5o6I5p2DIFNESyDpm4bmiJDkuoZRUSDlvq7kv6Eg5b6u5Y2aIEdpdGh1YuetieW4uOeUqOaOpeWPoyDmlK/mjIEgcGhwIGZwbSDlkowgU3dvb2xlIOaJgOacieahhuaetumAmueUqCBRUee+pCAxNzkxNjIyN+aKlumfs+WOu+awtOWNsFBIUOeJiOaOpeWPo+S4gOS4quWIhuW4g+W8j+e7n+iuoeebkeaOp+ezu+e7nyDljIXlkKtQSFDlrqLmiLfnq68g5pyN5Yqh56uv5pW05ZCI5aSa5o6l5Y+j55qESVDmn6Xor6Llt6Xlhbcg5Z+65LqO6Zi/6YeM5LqRT1NT55qEV29yZFByZXNz6L+c56iL6ZmE5Lu25pSv5oyB5o+S5Lu2IOWQjuS8muacieacnyDlvIDnrrHljbPnlKjnmoRMYXJhdmVs5ZCO5Y+w5omp5bGVIOWJjeWQjuerr+WIhuemuyDlkI7nq6/mjqfliLbliY3nq6/nu4Tku7Yg5peg6ZyA57yW5YaZdnVl5Y2z5Y+v5Yib5bu65LiA5Liq55qE6aG555uuIOS4sOWvjOeahOihqOWNlSDooajmoLznu4Tku7Yg5by65aSn55qE6Ieq5a6a5LmJ57uE5Lu25Yqf6IO9IHlpaTIgc3dvb2xlIOiuqXlpaTLov5DooYzlnKhzd29vbGXkuIrog5bpvKDph4fpm4YgV29yZFByZXNz5LyY56eA5byA5rqQ6YeH6ZuG5o+S5Lu2Q2F0Y2hBZG1pbuaYr+S4gOasvuWfuuS6jnRoaW5rcGhwNiDlkowgZWxlbWVudCBhZG1pbiDlvIDlj5HnmoTlkI7lj7DnrqHnkIbns7vnu58g5Z+65LqOIFNlcnZpY2VQcm92aWRlciDns7vnu5/mqKHlnZflrozlhajmjqXogKYg6ZqP5pe25Y246L295a6J6KOF5qih5Z2XIOaPkOS+m+S6huWujOaVtOeahOadg+mZkOWSjOaVsOaNruadg+mZkOetieWKn+iDvSDlpKfph4/lhoXnva7nmoTlvIDlj5Hlt6Xlhbfmj5DljYfkvaDnmoTlvIDlj5HkvZPpqowg5a6Y572R5Zyw5Z2AIOW+ruS/oeWFrOS8l+W5s+WPsHBocOeJiOW8gOWPkeWMheW+ruS/oeWwj+eoi+W6jyDmoKHlm63lsI/mg4XkuablkI7lj7DmupDnoIEg5aW9546p55qE6KGo55m95aKZIOWRiueZveWimSDlip/og73lhajpnaLnmoRQSFDlkb3ku6TooYzlupTnlKjlupMg5o+Q5L6b5o6n5Yi25Y+w5Y+C5pWw6Kej5p6QIOWRveS7pOi/kOihjCDpopzoibLpo47moLzovpPlh7og55So5oi35L+h5oGv5Lqk5LqSIOeJueauiuagvOW8j+S/oeaBr+aYvuekuuWfuuS6jiBjaGluZXNlIHBvZXRyeSDmlbDmja7mlbTnkIbnmoTkuIDku70gbXlzcWwg5qC85byP5pWw5o2u5biu5YqpIHRoaW5rcGhwIDUg5byA5Y+R6ICF5b+r6YCfIOi9u+advueahOaehOW7ukFwaSBoeXBlcmYgYWRtaW4g5piv5Z+65LqOIGh5cGVyZiB2dWUg55qE6YWN572u5YyW5ZCO5Y+w5byA5Y+R5bel5YW3IOW+ruS/oeaUr+S7mHBocCDlhpnnmoTop4bpopHkuIvovb3lt6Xlhbcg546w5bey5pSv5oyBIFlvdWt1IE1pYW9wYWkg6IW+6K6vIFhWaWRlb3MgUG9ybmh1YiA5MXBvcm4g5b6u5Y2a6YW354eDIGJpbGliaWxpIOS7iuaXpeWktOadoSDoipLmnpxUVkNvcmVQcmVzcyDkuLvpopgg5LiA5qy+6auY5oCn6IO9IOmrmOminOWAvOeahFdvcmRQcmVzc+S4u+mimOW/q+mTvueUteWVhiDnm7Tmkq3nlLXllYYg5YiG6ZSA5ZWG5Z+OIOW+ruS/oeWwj+eoi+W6j+WVhuWfjiBBUFDllYbln44g5YWs5LyX5Y+35ZWG5Z+OIFBD5ZWG5Z+O57O757ufIOaUr+S7mOWuneWwj+eoi+W6j+WVhuWfjiDmipbpn7PlsI/nqIvluo/llYbln44g55m+5bqm5bCP56iL5bqP55S15ZWG57O757ufIOWJjeWQjuerr+S7o+eggeWFqOmDqOW8gOa6kCBMYXJhdmVsIHZ1ZeW8gOWPkSDmiJDnhp/llYbnlKjpobnnm64gc2hvcCBtYWxsIOWVhuWfjiDnlLXllYYg5Yip55SoIFBIUCBjVVJMIOi9rOWPkSBEaXNxdXMgQVBJIOivt+axguWPr+iDveaYr+acgOS8mOmbhSDnroDmmJPnmoTmt5jlrp3lrqJTREtVbmlBZG1pbuaYr+S4gOWll+a4kOi/m+W8j+aooeWdl+WMluW8gOa6kOWQjuWPsCDph4fnlKjliY3lkI7nq6/liIbnprvmioDmnK8g5pWw5o2u5Lqk5LqS6YeH55SoanNvbuagvOW8jyDlip/og73kvY7ogKblkIjpq5jlhoXogZog5qC45b+D5qih5Z2X5pSv5oyB57O757uf6K6+572uIOadg+mZkOeuoeeQhiDnlKjmiLfnrqHnkIYg6I+c5Y2V566h55CGIEFQSeeuoeeQhuetieWKn+iDvSDlkI7mnJ/kuIrnur/mqKHlnZfllYbln47lsIbmiZPpgKDnsbvkvLxjb21wb3NlciBucG3nmoTlvIDmlL7lvI/mj5Lku7bluILlnLog5ZCM5pe25oiR5Lus5bCG5omT6YCg5LiA5aWX5YW85a655oCn55qEQVBJ5qCH5YeGIOS7jlRoaW5rUEhQNSAxIFZ1ZTLlvIDlp4sg6YCQ5q2l5ZC45byV54ix5aW96ICF5YWx5ZCM5Yqg5YWlIOS7peimhuebluetieWkmuivreiogOahhuaetiBQSFAg5aSa5o6l5Y+j6I635Y+W5b+r6YCS54mp5rWB5L+h5oGv5YyFTGlnaHRDTVMg5piv5LiA5Liq5Z+65LqOIExhcmF2ZWwg5byA5Y+R55qE6L276YeP57qnIENNUyDns7vnu58g5Lmf5Y+v5Lul5L2c5Li65LiA5Liq6YCa55So55qE5ZCO5Y+w566h55CG5qGG5p625L2/55So5Y2V54K555m75b2V57O757uf5b+r5LmQ5LqM57qn5Z+f5ZCN5YiG5Y+R57O757ufVHlwZWNobyBUaGVtZSBTdG9yeSDniLHkuIrkvaDmiJHnmoTmlYXkuosg5LiA5Liq6L276YeP5YyW55qE55WZ6KiA5p2/IOiusOS6i+acrCDnpL7kuqTns7vnu58g5Y2a5a6iIOS6uuexu+eahOacrOi0qOaYryDlkpXlkpXlkpXvvJ/lvq7kv6Hln5/lkI3mi6bmiKrmo4DmtYsgUVHln5/lkI3mi6bmiKrmo4DmtYsgdCB4emt4YiBjb20g5p+l6K+i5pyJ57yT5a2YIOWmgumcgOWunuaXtuafpeivouivt+iHquihjOmDqOe9siDpq5jmgKfog73liIbluIPlvI/lubblj5HplIEg6KGM5Li66ZmQ5rWBRW1sb2fmmK/kuIDmrL7ln7rkuo5QSFDlkoxNeVNRTOeahOWKn+iDveW8uuWkp+eahOWNmuWuouWPikNNU+W7uuermeezu+e7nyDov73msYLlv6vpgJ8g56iz5a6aIOeugOWNlSDoiJLpgILnmoTlu7rnq5nkvZPpqoxIeXBlcmYgYWRtaW4g5Z+65LqOSHlwZXJmIEVsZW1lbnQgVUkg6YCa55So566h55CG5ZCO5Y+w5LyB5Lia5LuT5bqT566h55CG57O757ufSGlzaVBIUCBWMueJiOaYr+WfuuS6jlRoaW5rUEhQNSAx5ZKMTGF5dWnlvIDlj5HnmoTlkI7lj7DmoYbmnrYg5om/6K+65rC45LmF5YWN6LS55byA5rqQIOaCqOWPr+eUqOS6juWtpuS5oOWSjOWVhueUqCDkvYbpobvkv53nlZnniYjmnYPkv6Hmga/mraPluLjmmL7npLog5aaC5p6cSGlzaVBIUOWvueaCqOacieW4ruWKqSDmgqjlj6/ku6Xngrnlh7vlj7PkuIrop5IgU3RhciDmlK/mjIHkuIDkuIvlk6Yg6LCi6LCiIOS9v+eUqFBIUOW8gOWPkeeahOeugOe6puWvvOiIqiDkuabnrb7nrqHnkIbns7vnu58g6L2v5pOO5piv5Z+65LqOIFBocCA3IDIg5ZKMIFN3b29sZSA0IDQg55qE6auY5oCn6IO9IOeugOWNleaYk+eUqOeahOW8gOWPkeahhuaetiDmlK/mjIHlkIzml7blnKggU3dvb2xlIFNlcnZlciDlkowgcGhwIGZwbSDkuKTnp43mqKHlvI/kuIvov5DooYwg5YaF572u5LqG5pyN5YqhIOmbhuaIkOS6huWkp+mHj+aIkOeGn+eahOe7hOS7tiDlj6/ku6XnlKjkuo7mnoTlu7rpq5jmgKfog73nmoRXZWLns7vnu58gQVBJIOS4remXtOS7tiDln7rnoYDmnI3liqHnrYnnrYkg5Liq5Lq65Y+R5Y2h5rqQ56CBIOWPkeWNoeezu+e7nyDkuozmrKHlhYPlj5HljaHns7vnu58g5LqM5qyh5YWD5Y+R5Y2h5rqQ56CBIOWPkeWNoeeoi+W6jyDliqjmvKvlj5HljaEgUEhQ5Y+R5Y2h5rqQ56CB6IGK5aSp5bqU55SoIHBocOWunueOsOeahGRodOeIrOiZq+aQreW7uueahHdlYmlt5a6i5pyN57O757ufIOWNs+aXtumAmuiur+S4gOS6m+WunueUqOeahHB5dGhvbuiEmuacrOWQjOWfjuaLvOi9puW+ruS/oeWwj+eoi+W6j+WQjuerr+S7o+eggSDkuIDkuKrlk43lupTlvI/lubLlh4DlkoznroDmtIHkvJjpm4XnmoQgVHlwZWNobyDkuLvpophwaHDku5PlupPov5vplIDlrZjmt7HluqblrabkuaA1IOmXriDku6Xpl67nrZTlvaLlvI/lr7nluLjnlKjnmoTmpoLnjofnn6Xor4Yg57q/5oCn5Luj5pWwIOacuuWZqOWtpuS5oCDmt7HluqblrabkuaAg6K6h566X5py66KeG6KeJ562J54Ot54K56Zeu6aKY6L+b6KGM6ZiQ6L+wIOS7peW4ruWKqeiHquW3seWPiuaciemcgOimgeeahOivu+iAhSDlhajkuabliIbkuLoxOOS4queroOiKgiA1IOS9meS4h+WtlyDnlLHkuo7msLTlubPmnInpmZAg5Lmm5Lit5LiN5aal5LmL5aSE5oGz6K+35bm/5aSn6K+76ICF5om56K+E5oyH5q2jIOacquWujOW+hee7rSDlpoLmnInmhI/lkIjkvZwg6IGU57O7c2N1dGp5MiAxNSAxNjMgY29tIOeJiOadg+aJgOaciSDov53mnYPlv4XnqbYgVGFuIDIgMTggNumimOinoyDorrDlvZXoh6rlt7HnmoRsZWV0Y29kZeino+mimOS5i+i3ryDmnIDlhajkuK3ljY7lj6Tor5for43mlbDmja7lupMg5ZSQ5a6L5Lik5pyd6L+R5LiA5LiH5Zub5Y2D5Y+k6K+X5Lq6IOaOpei/kTUgNeS4h+mmluWUkOivl+WKoDI25LiH5a6L6K+XIOS4pOWui+aXtuacnzE1NjTkvY3or43kurogMjEgNSDpppbor40gdW5pIGFwcCDmmK/kvb/nlKggVnVlIOivreazleW8gOWPkeWwj+eoi+W6jyBINSBBcHDnmoTnu5/kuIDmoYbmnrbph4fnlKjoh6rouqvmqKHlnZfop4TojIPnvJblhpnnmoTliY3nq68gVUkg5qGG5p62IOmBteW+quWOn+eUnyBIVE1MIENTUyBKUyDnmoTkuablhpnlvaLlvI8g5p6B5L2O6Zeo5qebIOaLv+adpeWNs+eUqCDmiJHmmK/kvp3miawg5pyo5piT5p2oIOWFrOS8l+WPtyDpq5jnuqfliY3nq6/ov5vpmLYg5L2c6ICFIOavj+WkqeaQnuWumuS4gOmBk+WJjeerr+Wkp+WOgumdouivlemimCDnpZ3lpKflrrblpKnlpKnov5vmraUg5LiA5bm05ZCO5Lya55yL5Yiw5LiN5LiA5qC355qE6Ieq5bexIFlBcGkg5piv5LiA5Liq5Y+v5pys5Zyw6YOo572y55qEIOaJk+mAmuWJjeWQjuerr+WPilFB55qEIOWPr+inhuWMlueahOaOpeWPo+euoeeQhuW5s+WPsOWwj+eoi+W6j+e7hOS7tuWMluW8gOWPkeahhuaetue9keaYk+S6kemfs+S5kCBOb2RlIGpzIEFQSSBzZXJ2aWNl5Z+65LqOIFZ1ZSBqcyDnmoTlsI/nqIvluo/lvIDlj5HmoYbmnrYg5LuO5bqV5bGC5pSv5oyBIFZ1ZSBqcyDor63ms5XlkozmnoTlu7rlt6XlhbfkvZPns7sgIEVDTUFTY3JpcHQgNuWFpemXqCDmmK/kuIDmnKzlvIDmupDnmoQgSmF2YVNjcmlwdCDor63oqIDmlZnnqIsg5YWo6Z2i5LuL57uNIEVDTUFTY3JpcHQgNiDmlrDlop7nmoTor63ms5XnibnmgKcgIOiwt+eykiBDaHJvbWXmj5Lku7boi7Hpm4Tmppwg5Li65LyY56eA55qEQ2hyb21l5o+S5Lu25YaZ5LiA5pys5Lit5paH6K+05piO5LmmIOiuqUNocm9tZeaPkuS7tuiLsembhOS7rOmAoOemj+S6uuexu+WFrOS8l+WPtyDliqAxIOWQjOatpeabtOaWsOWJjeerr+mdouivleavj+aXpSAzIDEg5Lul6Z2i6K+V6aKY5p2l6amx5Yqo5a2m5LmgIOaPkOWAoeavj+aXpeWtpuS5oOS4juaAneiAgyDmr4/lpKnov5vmraXkuIDngrkg5q+P5aSp5pep5LiKNeeCuee6r+aJi+W3peWPkeW4g+mdouivlemimCDmrbvno5Xoh6rlt7Eg5oSJ5oKm5aSn5a62IDQg6YGT5YmN56uv6Z2i6K+V6aKY5YWo6Z2i6KaG55uW5bCP56iL5bqPIOi9r+aKgOiDvSDmnKzmlofljp/mlofnlLHnn6XlkI0gSGFja2VyIEVyaWMgUyBSYXltb25kIOaJgOaSsOWvqyDmlZnkvaDlpoLkvZXmraPnorrnmoTmj5Dlh7rmioDooZPllY/poYzkuKbnjbLlvpfkvaDmu7/mhI/nmoTnrZTmoYgg5Y2D5Y+k5YmN56uv5Zu+5paH5pWZ56iLIOi2heivpue7hueahOWJjeerr+WFpemXqOWIsOi/m+mYtuWtpuS5oOeslOiusCDku47pm7blvIDlp4vlrabliY3nq68g5YGa5LiA5ZCN57K+6Ie05LyY6ZuF55qE5YmN56uv5bel56iL5biIIOWFrOS8l+WPtyDljYPlj6Tlo7nlj7cg5L2c6ICFICBib29rIE5vZGUganMg5YyF5pWZ5LiN5YyF5LyaIGJ5IGFsc290YW5n5pS26ZuG5omA5pyJ5Yy65Z2X6ZO+IEJsb2NrQ2hhaW4g5oqA5pyv5byA5Y+R55u45YWz6LWE5paZIOWMheaLrEZhYnJpY+WSjEV0aGVyZXVt5byA5Y+R6LWE5paZ6L276YePIOWPr+mdoOeahOWwj+eoi+W6jyBVSSDnu4Tku7blupPlvq7kv6HlsI/nqIvluo/llYbln44g5b6u5L+h5bCP56iL5bqP5b6u5bqX5LiA5Liq5Y+v5Lul6KeC55yL5Zu95YaF5Li75rWB6KeG6aKR5bmz5Y+w5omA5pyJ6KeG6aKR55qE5a6i5oi356uv5Y+v5Ly457yp5biD5bGA5pa55qGI5Z+65LqOIG5vZGUganMgTW9uZ29kYiDmnoTlu7rnmoTlkI7lj7Dns7vnu58ganMg5rqQ56CB6Kej5p6Q56OB5Yqb6ZO+5o6l6IGa5ZCI5pCc57Si5Lit5Y2O5Lq65rCR5YWx5ZKM5Zu96KGM5pS/5Yy65YiSIOecgee6pyDnnIHku73nm7TovpbluILoh6rmsrvljLog5Zyw57qnIOWfjuW4giDljr/nuqcg5Yy65Y6/IOS5oee6pyDkuaHplYfooZfpgZMg5p2R57qnIOadkeWnlOS8muWxheWnlOS8miDkuK3lm73nnIHluILljLrplYfmnZHkuoznuqfkuInnuqflm5vnuqfkupTnuqfogZTliqjlnLDlnYDmlbDmja4gV2Vi5o6l5Y+j566h55CG5bel5YW3IOW8gOa6kOWFjei0uSDmjqXlj6Poh6rliqjljJYgTU9DS+aVsOaNruiHquWKqOeUn+aIkCDoh6rliqjljJbmtYvor5Ug5LyB5Lia57qn566h55CGIOmYv+mHjOWmiOWmiE1VWOWboumYn+WHuuWTgSDpmL/ph4zlt7Tlt7Tpg73lnKjnlKggMSDlhazlj7jnmoTpgInmi6kgUkFQMuW3suWPkeW4g+ivt+enu+atpeiHs2dpdGh1YiBjb20gdGh4IHJhcDIgZGVsb3NLdWJvYXJkIOaYr+WfuuS6jiBLdWJlcm5ldGVzIOeahOW+ruacjeWKoeeuoeeQhueVjOmdoiDlkIzml7bmj5DkvpsgS3ViZXJuZXRlcyDlhY3otLnkuK3mlofmlZnnqIsg5YWl6Zeo5pWZ56iLIOacgOaWsOeJiOacrOeahCBLdWJlcm5ldGVzIHYxIDIg5a6J6KOF5omL5YaMIGs4cyBpbnN0YWxsIOWcqOe6v+etlOeWkSDmjIHnu63mm7TmlrAgQXBhY2hlQ04g5pWw5o2u57uT5p6E5LiO566X5rOV6K+R5paH6ZuGIGNoaWNrIOaYr+S9v+eUqCBOb2RlIGpzIOWSjCBNb25nb0RCIOW8gOWPkeeahOekvuWMuuezu+e7n+S4gOS4qumdnuW4uOmAguWQiElU5Zui6Zif55qE5Zyo57q/QVBJ5paH5qGjIOaKgOacr+aWh+aho+W3peWFtyBDaGluZXNlIHN0aWNrZXIgcGFjayBNb3JlIGpveSDooajmg4XljIXnmoTljZrnianppoYgR2l0aHVi5pyA5pyJ5q+S55qE5LuT5bqTIOS4reWbveihqOaDheWMheWkp+mbhuWQiCDogZrmrKLkuZAg6auY6aKc5YC855qE56ys5LiJ5pa5572R5piT5LqR5pKt5pS+5ZmoIOaUr+aMgSBXaW5kb3dzIG1hY09TIExpbnV4IHZ1ZTIgdnVlIHJvdXRlciB2dWV4IOWFpemXqOmhueebrue9keaYk+S6kemfs+S5kOesrOS4ieaWuSBGbHV0dGVy5a6e5oiYIOeUteWtkOS5piDkuIDlpZfku6PnoIHov5DooYzlpJrnq68g5LiA56uv5omA6KeB5Y2z5aSa56uv5omA6KeBIOiuoeeul+acuumAn+aIkOivviBDcmFzaCBDb3Vyc2Ug5a2X5bmV57uEIOWFqDQg6ZuGIDIgMTggNSAxIOeyvuagoeWujOaIkCDkuIDkuKogcmVhY3QgcmVkdXgg55qE5a6M5pW06aG555uuIOWSjCDkuKrkurrmgLvnu5PkuK3mlofni6znq4vljZrlrqLliJfooahDU1MgSW5zcGlyYXRpb24g5Zyo6L+Z6YeM5om+5Yiw5YaZIENTUyDnmoTngbXmhJ8gcmljaCB0ZXh0IOWvjOaWh+acrOe8lui+keWZqCDmsYnlrZfmi7zpn7MgaMOgbiB6w6wgcMSrbiB5xKtuIENocm9tZeaPkuS7tuW8gOWPkeWFqOaUu+eVpSDphY3lpZflrozmlbREZW1vIOasoui/jmNsb25l5L2T6aqM5b6u5L+h6LCD6K+VIOWQhOenjVdlYlZpZXfmoLflvI/osIPor5Ug5omL5py65rWP6KeI5Zmo55qE6aG16Z2i55yf5py66LCD6K+VIOS+v+aNt+eahOi/nOeoi+iwg+ivleaJi+acuumhtemdoiDmipPljIXlt6Xlhbcg5pSv5oyBIEhUVFBTIOaXoOmcgFVTQui/nuaOpeiuvuWkhyAgbWFzdGVy5YiG5pSvIOa4suafk+WZqCDlvq7kv6HlsI/nqIvluo/nu4Tku7YgQVBJIOS6keW8gOWPkeekuuS+i+eugOaCpiBTaW1wUmVhZCDorqnkvaDnnqzpl7Tov5vlhaXmsonmtbjlvI/pmIXor7vnmoTmianlsZXorqlINeWItuS9nOWDj+aQreenr+acqOS4gOagt+eugOWNlSDovbvmnb7mkK3lu7pINemhtemdoiBINee9keermSBQQ+err+e9keermSBMb3dDb2Rl5bmz5Y+wICDkuIDlpZfnu4Tku7bljJYg5Y+v5aSN55SoIOaYk+aJqeWxleeahOW+ruS/oeWwj+eoi+W6jyBVSSDnu4Tku7blupPov5nmmK/kuIDkuKrmlbDmja7lj6/op4bljJbpobnnm64g6IO95aSf5bCG5Y6G5Y+y5pWw5o2u5o6S5ZCN6L2s5YyW5Li65Yqo5oCB5p+x54q25Zu+5Zu+6KGo5b6u5L+h5bCP56iL5bqP5Zu+6KGoY2hhcnRz57uE5Lu2IENoYXJ0cyBmb3IgV2VDaGF0IHNtYWxsIGFwcOexu+S8vOaYk+S8geengOeahEg15Yi25L2cIOW7uuermeW3peWFtyDlj6/op4bljJbmkK3lu7rns7vnu58g5LiA5Liq5Zyo5L2g57yW56iL5pe255av54uC56ew6LWe5L2g55qEIFZTQ29kZSDmianlsZXmj5Lku7blhajlrrbmobblkI7lj7DnrqHnkIbmoYbmnrbop6PplIHnvZHmmJPkupHpn7PkuZDlrqLmiLfnq6/lj5jngbDmrYzmm7Lnvo7op4LmmJPnlKjnmoRSZWFjdOWvjOaWh+acrOe8lui+keWZqCDln7rkuo5kcmFmdCBqc+W8gOWPkeS4gOS4quiHtOWKm+S6juW+ruS/oeWwj+eoi+W6j+WSjCBXZWIg56uv5ZCM5p6E55qE6Kej5Yaz5pa55qGI5b6e6Zu26ZaL5aeL5a24IFJlYWN0SlMgUmVhY3RKUyAxIDEg5piv5LiA5pys5biM5pyb6K6T5Yid5a246ICF5LiA55yL5bCx5oeC55qEIFJlYWN0IOS4reaWh+WFpemWgOaVmeWtuOabuCDnlLHmt7rlhaXmt7Hlrbjnv5IgUmVhY3RKUyDnlJ/mhYvns7vmupDnoIHop6Por7sg57O75YiX5paH56ugIOWujCDmiJHlsLHmmK/mnaXliIbkuqvohJrmnKznjqnnjqnnmoTlvIDlj5HogIXovrnovaYgZ2l0aHVi5omT5LiN5byAIGdpdGh1YuWKoOmAnyBnaXQgY2xvbmXliqDpgJ8gZ2l0IHJlbGVhc2XkuIvovb3liqDpgJ8gc3RhY2tvdmVyZmxvd+WKoOmAn3Z1Zea6kOeggemAkOihjOazqOmHiuWIhuaekCA0IOWkmm3nmoR2dWXmupDnoIHnqIvluo/mtYHnqIvlm77mgJ3nu7Tlr7zlm74gZGlmZumDqOWIhuW+heWQjue7reabtOaWsCDlvq7kv6HlsI/nqIvluo/op6PlhrPmlrnmoYggMUtCIGphdmFzY3JpcHQg6KaG55uW54q25oCB566h55CGIOi3qOmhtemAmuiuryDmj5Lku7blvIDlj5HlkozkupHmlbDmja7lupPlvIDlj5Hnu5nogIHlj7jmnLrnlKjnmoTkuIDkuKrnlarlj7fmjqjojZDns7vnu58gRmVIZWxwZXIgV2Vi5YmN56uv5Yqp5omL6K6w5b2V5oiQ6ZW/55qE6L+H56iL5ZOU5ZOp5ZOU5ZOpIGJpbGliaWxpIGNvbSDovoXliqnlt6Xlhbcg5Y+v5Lul5pu/5o2i5pKt5pS+5ZmoIOaOqOmAgemAmuefpeW5tui/m+ihjOS4gOS6m+W/q+aNt+aTjeS9nOaPkOS+m+S6hueZvuW6puWdkOaghyBCRCA5IOWbvea1i+WxgOWdkOaghyDngavmmJ/lnZDmoIcgR0NKIDIg5ZKMV0dTODTlnZDmoIfns7vkuYvpl7TnmoTovazmjaJGMmV0ZXN05piv5LiA5Liq6Z2i5ZCR5YmN56uvIOa1i+ivlSDkuqflk4HnrYnlspfkvY3nmoTlpJrmtY/op4jlmajlhbzlrrnmgKfmtYvor5XmlbTkvZPop6PlhrPmlrnmoYggIO+4jyDpmL/ph4zpo57njKog5b6I5piT55So55qE5Lit5ZCO5Y+wIOihqOWNlSDooajmoLwg5Zu+6KGoIOino+WGs+aWueahiENSTUVCIE1pbiDliY3lkI7nq6/liIbnprvniYjoh6rluKblrqLmnI3ns7vnu58g5pivQ1JNRULlk4HniYzlhajmlrDmjqjlh7rnmoTkuIDmrL7ovbvph4/nuqcg6auY5oCn6IO9IOWJjeWQjuerr+WIhuemu+eahOW8gOa6kOeUteWVhuezu+e7nyDlrozlloTnmoTlkI7lj7DmnYPpmZDnrqHnkIYg5Lya5ZGY566h55CGIOiuouWNleeuoeeQhiDkuqflk4HnrqHnkIYg5a6i5pyN566h55CGIENNU+euoeeQhiDlpJrnq6/nrqHnkIYg6aG16Z2iRElZIOaVsOaNrue7n+iuoSDns7vnu5/phY3nva4g57uE5ZCI5pWw5o2u566h55CGIOaXpeW/l+euoeeQhiDmlbDmja7lupPnrqHnkIYg5LiA6ZSu5byA6YCa55+t5L+hIOS6p+WTgemHh+mbhiDnianmtYHmn6Xor6LnrYnmjqXlj6MgIFJlYWN05oqA5pyv5o+t56eYIOS4gOacrOiHqumhtuWQkeS4i+eahFJlYWN05rqQ56CB5YiG5p6Q5Lmm5b6u5L+h5bCP56iL5bqPIOWfuuS6jndlcHkg5ZWG5Z+OIOW+ruW6lyDlvq7kv6HlsI/nqIvluo8g5qyi6L+O5a2m5Lmg5Lqk5rWB5aSn5bGP5pWw5o2u5Y+v6KeG5YyWUHl0b3JjaCDkuK3mlofmlofmoaPnu4/lhbjnmoTnvZHpobXlr7nor53moYbnu4Tku7Yg5by65aSn55qE5Yqo5oCB6KGo5Y2V55Sf5oiQ5ZmoIOeugOa0gSDmmJPnlKgg54G15rS755qE5b6u5L+h5bCP56iL5bqP57uE5Lu25bqTIOS4gOasviBNYXRlcmlhbCBEZXNpZ24g6aOO5qC855qEIEhleG8g5Li76aKY562+5Yiw5LiA5Liq5biu5Yqp5L2g6Ieq5Yqo55Sz6K+35Lqs5Lic5Lu35qC85L+d5oqk55qEY2hyb21l5ouT5bGV5ZCO5Y+wYWRtaW7liY3nq6/mqKHmnb8g5Z+65LqOIGxheXVpIOe8luWGmeeahOacgOeugOa0gSDmmJPnlKjnmoTlkI7lj7DmoYbmnrbmqKHmnb8g5Y+q6ZyA5o+Q5L6b5LiA5Liq5o6l5Y+j5bCx55u05o6l5Yid5aeL5YyW5pW05Liq5qGG5p62IOaXoOmcgOWkjeadguaTjeS9nCDlsI/nqIvluo/nlJ/miJDlm77niYflupMg6L275p2+6YCa6L+HIGpzb24g5pa55byP57uY5Yi25LiA5byg5Y+v5Lul5Y+R5Yiw5pyL5Y+L5ZyI55qE5Zu+54mH5a6J5Y2T5bqU55So5bGC5oqT5YyF6YCa5p2A6ISa5pys5LiA5Liq6L276YeP55qE5bel5YW36ZuG5ZCI5ama56S85aSn5bGP5LqS5YqoIOW+ruS/oeivt+afrOS4gOermeW8j+ino+WGs+aWueahiG1pbGkg5piv5LiA5Liq5byA5rqQ55qE56S+5Yy657O757ufIOeVjOmdouS8mOmbhSDlip/og73kuLDlr4wg5Lid6Iis6aG65ruR55qE6Kem5pG46L+Q5Yqo5pa55qGI5YGa5pyA5aW955qE5o6l5Y+j566h55CG5bmz5Y+wTXB4IOS4gOasvuWFt+acieS8mOengOW8gOWPkeS9k+mqjOWSjOa3seW6puaAp+iDveS8mOWMlueahOWinuW8uuWei+i3qOerr+Wwj+eoi+W6j+ahhuaetuecgeW4guWMuuWOv+S5oemVh+S4iee6p+aIluWbm+e6p+WfjuW4guaVsOaNriDluKbmi7zpn7PmoIfms6gg5Z2Q5qCHIOihjOaUv+WMuuWfn+i+ueeVjOiMg+WbtCAyIDIx5bm0IDfmnIggM+aXpeacgOaWsOmHh+mbhiDmj5Dkvptjc3bmoLzlvI/mlofku7Yg5pSv5oyB5Zyo57q/6L2s5oiQ5aSa57qn6IGU5YqoanPku6PnoIEg6YCa55SoanNvbuagvOW8jyDmj5Dkvpvova/ku7bovazmiJBzaHAgZ2VvanNvbiBzcWwg5a+85YWl5pWw5o2u5bqTIOW4pua1j+iniOWZqOmHjOmdoui/kOihjOeahGpz6YeH6ZuG5rqQ56CBIOe7vOWQiOS6huS4reWNjuS6uuawkeWFseWSjOWbveawkeaUv+mDqCDlm73lrrbnu5/orqHlsYAg6auY5b635Zyw5Zu+IOiFvuiur+WcsOWbvuihjOaUv+WMuuWIkuaVsOaNruWcqHZzY29kZeS4reeUqOS6jueUn+aIkOaWh+S7tuWktOmDqOazqOmHiuWSjOWHveaVsOazqOmHiueahOaPkuS7tiDnu4/ov4flpJrniYjov63ku6PlkI4g5o+S5Lu2IOaUr+aMgeaJgOacieS4u+a1geivreiogCDlip/og73lvLrlpKcg54G15rS75pa55L6/IOaWh+aho+m9kOWFqCDpo5/nlKjnroDljZUg6KeJ5b6X5o+S5Lu25LiN6ZSZ55qE6K+dIOeCueWHu+WPs+S4iuinkue7meS4qlN0YXIg77iP5ZGAICBKQVZDbHViIOiuqeS9oOeahOWkp+WnkOWnkOS4jeWGjei1sOS4ou+4j+S9oOaDs+imgeeahOacgOWFqCBBbmRyb2lkIOi/m+mYtui3r+e6v+efpeivhuWbvuiwsSDlubLotKfotYTmlpnmlLbpm4YgIOW8gOWPkeiAheaOqOiNkOmYheivu+eahOS5puexjSAyIDIg5reY5a6dIOS6rOS4nCDmlK/ku5jlrp3lj4zljYHkuIAg5Y+MMTHlhajmsJHlhbvnjKsg5YWo5rCR6JCl5Lia6Ieq5Yqo5YyW6ISa5pysIOWFqOmineWlluWKsSDpmLLmo4DmtYsg5LiA5qy+6auY5oCn6IO95pWP5oSf6K+NIOmdnuazleivjSDohI/lrZcg5qOA5rWL6L+H5ruk57uE5Lu2IOmZhOW4pue5geS9k+eugOS9k+S6kuaNoiDmlK/mjIHlhajop5LljYrop5LkupLmjaIg5rGJ5a2X6L2s5ou86Z+zIOaooeeziuaQnOe0ouetieWKn+iDvSDliY3nq6/ljZrlrqIg5YWz5rOo5Z+656GA55+l6K+G5ZKM5oCn6IO95LyY5YyWIHZ1ZSBjbGk06YWN572udnVlIGNvbmZpZyBqc+aMgee7reabtOaWsFBUIOWKqeaJiyBQbHVzIOS4uiBHb29nbGUgQ2hyb21lIOWSjCBGaXJlZm94IOa1j+iniOWZqOaPkuS7tiBXZWIgRXh0ZW5zaW9ucyDkuLvopoHnlKjkuo7ovoXliqnkuIvovb0gUFQg56uZ55qE56eN5a2QIOWfuuS6jnZ1ZTIga29hMueahCBINeWItuS9nOW3peWFtyDorqnkuI3kvJrlhpnku6PnoIHnmoTkurrkuZ/og73ovbvmnb7lv6vpgJ/kuIrmiYvliLbkvZxINemhtemdoiDnsbvkvLzmmJPkvIHnp4Ag55m+5bqmSDXnrYlINeWItuS9nCDlu7rnq5nlt6XlhbfmnIDlhajmnIDmlrDkuK3lm73nnIEg5biCIOWcsOWMumpzb27lj4pzcWzmlbDmja7pppbkuKogVGFybyDlpJrnq6/nu5/kuIDlrp7kvosg572R5piT5Lil6YCJIOWwj+eoi+W6jyBINSBSZWFjdCBOYXRpdmUgQnkg6Laj5bqXIEZFROWcsOeQhuS/oeaBr+WPr+inhuWMluW6k+S8geS4mue6pyBOb2RlIGpzIOW6lOeUqOaAp+iDveebkeaOp+S4jue6v+S4iuaVhemanOWumuS9jeino+WGs+aWueahiCBOb2RlIGpz5Yy65Z2X6ZO+5byA5Y+RIOazqCDmlrDniYjku6PnoIHlt7LlvIDmupAg6K+3c3RhcuaUr+aMgeWTpiDln7rkuo5BdXRvIGpz55qE6JqC6JqB5qOu5p6X6IO96YeP6Ieq5Yqo5pS25Y+W6ISa5pysIOe7k+W3tCDkuK3mlofliIbor43nmoROb2RlIGpz54mI5pysIOivkSDpnaLlkJHmnLrlmajlrabkuaDnmoTnibnlvoHlt6XnqIt3ZWJmdW5ueeaYr+S4gOasvui9u+mHj+e6p+eahOWJjeerr+ebkeaOp+ezu+e7nyB3ZWJmdW5ueeS5n+aYr+S4gOasvuWJjeerr+aAp+iDveebkeaOp+ezu+e7nyDml6Dln4vngrnnm5HmjqfliY3nq6/ml6Xlv5cg5a6e5pe25YiG5p6Q5YmN56uv5YGl5bq354q25oCB5LiA5Liq5a6e546w5rGJ5a2X5LiO5ou86Z+z5LqS6L2s55qE5bCP5bend2Vi5bel5YW35bqTIOa8lOekuuWcsOWdgCAg5YmN56uv6L+b6Zi2IOS8mOi0qOWNmuaWhyDkuIDkuKogQ2hyb21lIOaPkuS7tiDlsIYgR29vZ2xlIENETiDmm7/mjaLkuLrlm73lhoXnmoQgIFZ1ZSBFbGVtZW50VUnmnoTlu7rnmoRDTVPlvIDlj5HmoYbmnrbotoXlrozmlbTnmoRSZWFjdCBOYXRpdmXpobnnm64g5Yqf6IO95Liw5a+MIOmAguWQiOWtpuS5oOWSjOaXpeW4uOS9v+eUqCBHU1lHaXRodWJBcHDns7vliJfnmoTkvJjlir8g5oiR5Lus55uu5YmN5bey57uP5oul5pyJ5Zub5Liq54mI5pysIOWKn+iDvem9kOWFqCDpobnnm67moYbmnrblhoXmioDmnK/mtonlj4rpnaLlub8g5a6M5oiQ5bqm6auYIOmFjeWll+aWh+eroCDpgILlkIjlhajpnaLlrabkuaAg5a+55q+U5Y+C6ICDIOW8gOa6kEdpdGh1YuWuouaIt+err0FwcCDmm7Tlpb3nmoTkvZPpqowg5pu05Liw5a+M55qE5Yqf6IO9IOaXqOWcqOabtOWlveeahOaXpeW4uOeuoeeQhuWSjOe7tOaKpOS4quS6ukdpdGh1YiDmj5Dkvpvmm7Tlpb3mm7Tmlrnkvr/nmoTpqb7ovabkvZPpqozOoyDlkIzmrL5XZWV454mI5pys5ZCM5qy+Rmx1dHRlcueJiOacrCBodHRwcyBnaXRodWIgY29tIENhckd1IOe+pCDlroflrpnmnIDlvLrnmoTliY3nq6/pnaLor5XmjIfljZcgbHVjaWZlciByZW4gZmUgaW50ZXJ2aWV3IOW+ruaFleWwj+eoi+W6j+W8gOa6kOeJiCBXb3JkUHJlc3PniYjlvq7kv6HlsI/nqIvluo/lh73mlbDlvI/nvJbnqIvmjIfljJfkuK3mlofniYhOb2RlIGpz6Z2i6K+V6aKYIOS+p+mHjeWQjuerr+W6lOeUqOS4juWvuU5vZGXmoLjlv4PnmoTnkIbop6NqUXVlcnnmupDnoIHop6PmnpDlsI/nmb3lhaXlnZF2dWXkuInpg6jmm7Ig5YWz5LqOIHZ1ZSDlnKjlt6XkvZznmoTkvb/nlKjpl67popjmgLvnu5Mg6K+355yL5Y2a5a6iIHN1bnNlZWtlcnMgZ2l0aHViIGlvIOS4gOebtOS/neaMgeabtOaWsOWfuuS6jiBWdWUg55qEIFBXQSDop6PlhrPmlrnmoYgg5biu5Yqp5byA5Y+R6ICF5b+r6YCf5pCt5bu6IFBXQSDlupTnlKgg6Kej5Yaz5o6l5YWlIFBXQSDnmoTlkITnp43pl67pophUaGlua0NNRuaYr+S4gOasvuaUr+aMgVN3b29sZeeahOW8gOa6kOWGheWuueeuoeeQhuahhuaetiDln7rkuo5UaGlua1BIUOW8gOWPkSDlkIzml7bmlK/mjIFQSFAgRlBN5ZKMU3dvb2xl5Y+M5qih5byPIOiuqVdFQuW8gOWPkeabtOW/qyDlvq7kv6HlsI/nqIvluo/lm77niYfoo4Hliarlt6XlhbfliY3nq6/nn6Xor4bmnIjliIpOZXh0IFRlcm1pbmFs5piv5LiA5Liq6L276YeP57qn5aCh5Z6S5py657O757ufIOaYk+WuieijhSDmmJPkvb/nlKgg5pSv5oyBUkRQIFNTSCBWTkMgVGVsbmV0IEt1YmVybmV0ZXPljY/orq4g5b6u5L+h5bCP56iL5bqPIOaXpeWOhue7hOS7tiDln7rkuo4gdWVkaXRvcueahOabtOeOsOS7o+WMlueahOWvjOaWh+acrOe8lui+keWZqCDmlK/mjIFIVFRQU+WfuuS6jkphdmFTY3JpcHQgUmVhY3QgVnVlMueahOa1geeoi+Wbvue7hOS7tiDph4fnlKhTcHJpbmcgTXlCYXRpcyBTaGlyb+ahhuaetiDlvIDlj5HnmoTkuIDlpZfmnYPpmZDns7vnu58g5p6B5L2O6Zeo5qebIOaLv+adpeWNs+eUqCDorr7orqHkuYvliJ0g5bCx6Z2e5bi45rOo6YeN5a6J5YWo5oCnIOS4uuS8geS4muezu+e7n+S/nempvuaKpOiIqiDorqnkuIDliIfpg73lj5jlvpflpoLmraTnroDljZUgUVHnvqQgMzI0NzggMiA0IDE0NTc5OTk1MiAg5LiA5Liq5YmN56uv55qE5Y2a5a6iICDmmKXmnb7lrqLmnI0g5aSa5rig6YGT5pm66IO95a6i5pyN57O757ufIOW8gOa6kOWuouacjeezu+e7nyDmnLrlmajkurrlrqLmnI3kuIDkuKrlt6XkvZzmtYHlubPlj7DlsI/nqIvluo8g5bCP5ri45oiP5Lul5Y+KIFdlYiDpgJrnlKggQ2FudmFzIOa4suafk+W8leaTjiDlnKjnur/lt6Xlhbfnp5jnsY0g5Li65Zyo57q/5bel5YW35YaZ5LiA5pys5LyY6LSo6K+05piO5LmmIOiuqeWcqOe6v+W3peWFt+mAoOemj+S6uuexuyDliY3nq6/lhoXlj4Ig5pyJ5YWz5LqOSmF2YVNjcmlwdCDnvJbnqIvojIPlvI8g6K6+6K6h5qih5byPIOi9r+S7tuW8gOWPkeeahOiJuuacr+etieWkp+WJjeerr+iMg+eVtOWGheeahOefpeivhuWIhuS6qyDml6jlnKjluK7liqnliY3nq6/lt6XnqIvluIjku6zlpK/lrp7mioDmnK/ln7rnoYDku6XpgJrov4fkuIDnur/kupLogZTnvZHkvIHkuJrmioDmnK/pnaLor5UgIO+4jyB2Q2FyZHMg5Lit5Zu96buE6aG1IOS8mOWMliBpT1MgQW5kcm9pZCDmnaXnlLUg5L+h5oGv55WM6Z2i5L2T6aqM5ZCE5bmz5Y+w55qE5YiG5rWB6KeE5YiZIOWkjeWGmeinhOWImeWPiuiHquWKqOWMluiEmuacrCDln7rkuo52dWUyIOeahOWunuaXtuiBiuWkqemhueebriDlm77niYfliaroo4HkuIrkvKDnu4Tku7Yg562J56yU6K6w5b+r6YCf5YiG5LqrIEdvb2dsZURyaXZlIE9uZURyaXZlIOavj+aXpeaXtuaKpSDku6XliY3nq6/mioDmnK/kvZPns7vkuLrkuLvopoHliIbkuqvor77popgg5qC55o2uIOaWh+eroCDlt6Xlhbcg5paw6Ze7IOinhumikeWHoOWkp+adv+Wdl+S9nOS4uuS4u+imgeWIhuexuyDkuIDmrL7pq5jmlYgg6auY5oCn6IO955qE5bin5Yqo55S755Sf5oiQ5bel5YW3IOWBnOatoue7tOaKpCDkuIDkuKrlnKjnur/pn7PkuZDmkq3mlL7lmagg5LuFIFVJIOaXoOWKn+iDvSDlsI/nqIvluo/lr4zmlofmnKznu4Tku7Yg5pSv5oyB5riy5p+T5ZKM57yW6L6RIGh0bWwg5pSv5oyB5Zyo5b6u5L+hIFFRIOeZvuW6piDmlK/ku5jlrp0g5aS05p2h5ZKMIHVuaSBhcHAg5bmz5Y+w5L2/55So5Z+65LqOIGVsZWN0cm9uIHZ1ZSDlvIDlj5HnmoTpn7PkuZDmkq3mlL7lmagg55WM6Z2i5qih5Lu/UVHpn7PkuZAg5oqA5pyv5qCI5qyi6L+Oc3RhcndldWkg5piv5Zyod2V1aeWSjHplcHRv5Z+656GA5LiK5byA5Y+R55qE5aKe5by6VUnnu4Tku7Yg55uu5YmN5YiG5Li66KGo5Y2VIOWfuuehgCDnu4Tku7YganPmj5Lku7blm5vlpKfnsbsg5YWx6K6h55m+5L2Z6aG55Yqf6IO9IOaYr+acgOWFqOeahHdldWnmoLflvI/lkIzmraXlkozmm7TmlrDlpKfkvazohJrmnKzlupMg5pu05paw5oeS5Lq66YWN572u6IW+6K6v5LqR5Y2z5pe26YCa5L+hIElNIOacjeWKoSDlm73lhoXkuIvovb3plZzlg48g5Z+65LqOIFZ1ZSDnmoTlsI/nqIvluo/lvIDlj5HmoYbmnrZSZWFjdCAxNiA45omT6YCg57K+576O6Z+z5LmQV2ViQXBwV0vns7vliJflvIDlj5HmoYbmnrYgVjHoh7NWNSBKYXZh5byA5rqQ5LyB5Lia57qn5byA5Y+R5qGG5p62IOWNleW6lOeUqCDlvq7mnI3liqEg5YiG5biD5byPICDvuI/kuK3lm70g55yB5biC5Yy6IOS4iee6p+iBlOWKqCDlnLDlnYDpgInmi6nlmagg5b6u5L+h5bCP56iL5bqPMmTliqjnlLvlupMg5YiG5biD5byPIFJlZGlz57yT5a2YIFNoaXJv5p2D6ZmQ566h55CGIFNwcmluZyBTZXNzaW9u5Y2V54K555m75b2VIFF1YXJ0euWIhuW4g+W8j+mbhue+pOiwg+W6piBSZXN0ZnVs5pyN5YqhIFFRIOW+ruS/oeeZu+W9lSBBcHAgdG9rZW7nmbvlvZUg5b6u5L+hIOaUr+S7mOWuneaUr+S7mCDml6XmnJ/ovazmjaIg5pWw5o2u57G75Z6L6L2s5o2iIOW6j+WIl+WMliDmsYnlrZfovazmi7zpn7Mg6Lqr5Lu96K+B5Y+356CB6aqM6K+BIOaVsOWtl+i9rOS6uuawkeW4gSDlj5HpgIHnn63kv6Eg5Y+R6YCB6YKu5Lu2IOWKoOWvhuino+WvhiDlm77niYflpITnkIYgZXhjZWzlr7zlhaXlr7zlh7ogRlRQIFNGVFAgZmFzdERGU+S4iuS8oOS4i+i9vSDkuoznu7TnoIEgWE1M6K+75YaZIOmrmOeyvuW6puiuoeeulyDns7vnu5/phY3nva7lt6XlhbfnsbvnrYnnrYkgRWR1U29obyDnvZHnu5zor77loILmmK/nlLHmna3lt57pmJTnn6XnvZHnu5znp5HmioDmnInpmZDlhazlj7jnoJTlj5HnmoTlvIDmupDnvZHmoKHns7vnu58gRWR1U29obyDljIXlkKvkuoblnKjnur/mlZnlraYg5oub55Sf5ZKM566h55CG562J5a6M5pW05Yqf6IO9IOiuqeaVmeiCsuacuuaehOWPr+S7pembtumXqOanm+W7uueri+e9keagoSDmiJDlip/ovazlnovlnKjnur/mlZnogrIgRWR1U29obyDkuZ/lj6/kvZzkuLrkvIHkuJrlhoXorq3lubPlj7Ag5biu5Yqp5LyB5Lia5a6e546w5Lq65omN5Z+55YW7ICDoh6rnlKjnmoTkuIDkupvkubHkuIPlhavns58g5rK554y06ISa5pysIOS4uuWImuWImuWtpuS5oHBocOivreiogOS7peWPindlYue9keermeW8gOWPkeaVtOeQhueahOS4gOWll+i1hOa6kCDmnInop4bpopEg5a6e5oiY5Luj56CBIOWtpuS5oOi3r+W+hOetiSDkvJrmjIHnu63mm7TmlrAgVGhpcyBpcyBhIGdvaW5kZXggdGhlbWUg5LiA5LiqZ29pbmRleOeahOaJqeWxleS4u+mimCBOdW1QeeWumOaWueS4reaWh+aWh+ahoyDlrozmlbTniYggIOaQreW7uuenu+WKqOerr+W8gOWPkSDln7rkuo7pgILphY3mlrnmoYggYXhpb3PlsIHoo4Ug5p6E5bu65omL5py656uv5qih5p2/6ISa5omL5p62IOWQjuWPsOeuoeeQhiDohJrmiYvmnrbmjqXlj6Mg5LuO566A5Y2V5byA5aeLIFBoYWxBcGnnroDnp7DPgOahhuaetiDkuIDkuKrovbvph4/nuqdQSFDlvIDmupDmjqXlj6PmoYbmnrYg5LiT5rOo5LqO5o6l5Y+j5pyN5Yqh5byA5Y+RIOWJjeerr+eJueaViOWtmOaho0Nocm9tZea1j+iniOWZqCDmiqLotK0g56eS5p2A5o+S5Lu2IOenkuadgOWKqeaJiyDlrprml7boh6rliqjngrnlh7vkupHlrZjlgqjnrqHnkIblrqLmiLfnq68g5pSv5oyB5LiD54mb5LqRIOiFvuiur+S6kSDpnZLkupEg6Zi/6YeM5LqRIOWPiOaLjeS6kSDkuprpqazpgIpTMyDkuqzkuJzkupEg5Lu/5paH5Lu25aS5566h55CGIOWbvueJh+mihOiniCDmi5bmi73kuIrkvKAg5paH5Lu25aS55LiK5LygIOWQjOatpSDmibnph4/lr7zlh7pVUkznrYnlip/og71mb250IGNhcnJpZXLmmK/kuIDkuKrlip/og73lvLrlpKfnmoTlrZfkvZPmk43kvZzlupMg5L2/55So5a6D5L2g5Y+v5Lul6ZqP5b+D5omA5qyy55qE5pON5L2c5a2X5L2TIOiuqeS9oOWPr+S7peWcqHN2Z+eahOe7tOW6puaUuemAoOWtl+S9k+eahOWxleeOsOW9oueKtiBDUk7mmK9DdHJpcCBSZWFjdCBOYXRpdmXnroDnp7Ag55Sx5pC656iL5peg57q/5bmz5Y+w56CU5Y+R5Zui6Zif5Z+65LqOUmVhY3QgTmF0aXZl5qGG5p625LyY5YyWIOWumuWItuaIkOeos+WumuaAp+WSjOaAp+iDveabtOS9syDkuZ/mm7TpgILlkIjkuJrliqHlnLrmma/nmoTot6jlubPlj7DlvIDlj5HmoYbmnrYgIOayueeMtOiEmuacrOmhtemdoua1rueql+W5v+WRiuWujOWFqOi/h+a7pOWHgOWMliDlm73mnI3mnIDlvLrmnIDlhajmnIDmlrBDU0RO6ISa5pys5b6u5L+h5bCP56iL5bqP5Y2z5pe26YCa6K6v5qih5p2/IOS9v+eUqFdlYlNvY2tldOmAmuS/oeWwj+eoi+W6j+WPjee8luivkSDmlK/mjIHliIbljIUg4oCc5oOz5a2m5ZCX4oCd5Liq5Lq655+l6K+G566h55CG5LiO6Ieq5aqS5L2T6JCl6ZSA5bel5YW3IOi2heWkmue7j+WFuCBDYW52YXMg5a6e5L6LIOWKqOaAgeemu+WtkOiDjOaZryDngqvlvanlsI/nkIMg6LSq5ZCD6JuHIOWdpuWFi+Wkp+aImCDmmK/nlLfkurrlsLHkuIsxIOWxgiDlv4PlvaLmloflrZfnrYkgIFZ1ZSBVRWRpdG9yIHYgbW9kZWzlj4zlkJHnu5HlrpogSFFDaGFydCBINSDlvq7kv6HlsI/nqIvluo8g5rKq5rexIOa4r+iCoSDmlbDlrZfotKfluIEg5pyf6LSnIOe+juiCoSBL57q/5Zu+IGtsaW5lIOi1sOWKv+WbviDnvKnmlL4g5ouW5ou9IOWNgeWtl+WFieaghyDnlLvlm77lt6Xlhbcg5oiq5Zu+IOetueeggeWbviDliIbmnpDlrrbor63ms5Ug6YCa6L6+5L+h6K+t5rOVIOm6puivreazlSDnrKwz5pa55pWw5o2u5pu/5o2i5o6l5Y+j5Z+65LqOa29hMueahOagh+WHhuWJjeWQjuerr+WIhuemu+ahhuaetiDkuIDmrL7kvIHkuJrkv6Hmga/ljJblvIDlj5Hln7rnoYDlubPlj7Ag5ouf6ZuG5oiQT0Eg5Yqe5YWs6Ieq5Yqo5YyWIENNUyDlhoXlrrnnrqHnkIbns7vnu58g562J5LyB5Lia57O757uf55qE6YCa55So5Lia5Yqh5Yqf6IO9IEplZVBsYXRmb3Jt6aG555uu5piv5LiA5qy+5LulU3ByaW5nQm9vdOS4uuaguOW/g+ahhuaetiDpm4ZPUk3moYbmnrZNeWJhdGlzIFdlYuWxguahhuaetlNwcmluZ01WQ+WSjOWkmuenjeW8gOa6kOe7hOS7tuahhuaetuiAjOaIkOeahOS4gOasvumAmueUqOWfuuehgOW5s+WPsCDku6PnoIHlt7Lnu4/mjZDotaDnu5nlvIDmupDkuK3lm73npL7ljLrln7rkuo5pbmNlcHRpb27nmoToh6rliqjljJZTUUzmk43kvZzlubPlj7Ag5pSv5oyBU1FM5omn6KGMIExEQVDorqTor4Eg5Y+R6YKu5Lu2IE9TQyBTUUzmn6Xor6IgU1FM5LyY5YyW5bu66K6uIOadg+mZkOeuoeeQhuetieWKn+iDvSDmlK/mjIFkb2NrZXLplZzlg4/mmK/kuIDmrL7kuJPpl6jpnaLlkJHkuKrkurog5Zui6Zif5ZKM5bCP5Z6L57uE57uH55qE56eB5pyJ572R55uY57O757ufIOi9u+mHjyDlvIDmupAg5a6M5ZaEIOaXoOiuuuaYr+WcqOWutuW6rSDlrabmoKHov5jmmK/lnKjlip7lhazlrqQg5oKo6YO96IO956uL5Yi75byA5aeL5L2/55So5a6DIOS6huino+abtOWkmuivt+iuv+mXruWumOaWuee9keermSBOb2RlIGpzIEFQSSDkuK3mlofmlofmoaNkdWJib+acjeWKoeeuoeeQhuS7peWPiuebkeaOp+ezu+e7n+aLr+aVkULnq5nnmoTlvLnluZXkvZPpqowgIOWvueaKl+WBh+a2iOaBr+ezu+WIl+mhueebruS5i+S4gCDmiKrlsY8g5a6e6ZSk77yf55u45L+h5L2g5bCx6L6T5LqGIOKAneeqgeegtOaAp+KAnOabtOaWsCDmlK/mjIHkv67mlLnku7vkvZXnvZHnq5kgIO+4j+S4gOS4queugOa0gSDkvJjpm4XkuJTpq5jmlYjnmoQgSHVnbyDkuLvpophWdWUganMg56S65L6L6aG555uuIOeugOaYk+eVmeiogOadvyDmnKzpobnnm67mi6XmnInlrozlloTnmoTmlofmoaPor7TmmI7kuI7ms6jph4og6K6p5oKo5b+r6YCf5LiK5omLIFZ1ZSBqcyDlvIDlj5E/IFZ1ZSBWYWxpZGF0b3I/IFZ1ZXg/5pyA5L2z5a6e6Le15Z+65LqOIE5vZGUganMgS29hMiDlrp7miJjlvIDlj5HnmoTkuIDlpZflrozmlbTnmoTljZrlrqLpobnnm67nvZHnq5kg55SoIFJlYWN0IOe8luWGmeeahOWfuuS6jlRhcm8gRHZh5p6E5bu655qE6YCC6YWN5LiN5ZCM56uvIOW+ruS/oSDnmb7luqYg5pSv5LuY5a6d5bCP56iL5bqPIEg1IFJlYWN0IE5hdGl2ZSDnrYkg55qE5pe26KOF6KGj5qmx5L+h5oGv5rOE5ryP55uR5o6n57O757ufIOS8quijhTExNea1j+iniOWZqOW5sueIhuWJjeerryDkuIDnvZHmiZPlsL3liY3nq6/pnaLor5Ug5a2m5Lmg6Lev5b6EIOS8mOengOWlveaWh+etieWQhOexu+WGheWuuSDluK7liqnlpKflrrbkuIDlubTlhoXmi7/liLDmnJ/mnJvnmoQgb2ZmZXIg5YmN56uv5oCn6IO955uR5o6n57O757ufIOa2iOaBr+mYn+WIlyDpq5jlj6/nlKgg6ZuG576k562J55u45YWz5p625p6EU3ByaW5nQm9vdCB2MumhueebruaYr+WKquWKm+aJk+mAoHNwcmluZ2Jvb3TmoYbmnrbnmoTmnoHoh7Tnu4bohbvnmoTohJrmiYvmnrYg5YyF5ous5LiA5aWX5ryC5Lqu55qE5YmN5Y+wIOaXoOWFtuS7luadguS4g+adguWFq+eahOWKn+iDvSDljp/nlJ/nuq/lh4Ag5Lit5paH5paH5pys5qCH5rOo5bel5YW3IOacgOWFqOacgOaWsOS4reWbvSDnnIEg5biCIOWMuuWOvyDkuaHplYfooZfpgZMganNvbiBjc3Ygc3Fs5pWw5o2uIOS4gOasvui9u+W3p+eahOa4kOi/m+W8j+W+ruS/oeWwj+eoi+W6j+ahhuaetiDlhajnvZEgMSB3IOmYheivu+mHj+eahOi/m+mYtuWJjeerr+aKgOacr+WNmuWuouS7k+W6kyBWdWUg5rqQ56CB6Kej5p6QIFJlYWN0IOa3seW6puWunui3tSBUeXBlU2NyaXB0IOi/m+mYtuiJuuacryDlt6XnqIvljJYg5oCn6IO95LyY5YyW5a6e6Le1IOWujOaVtOW8gOa6kCBKYXZh5b+r6YCf5byA5Y+R5bmz5Y+wIOWfuuS6jlNwcmluZyBTcHJpbmdNVkMgTXliYXRpc+aetuaehCBNU3RvcmXmj5Dkvpvmm7TlpJrlpb3nlKjnmoTmj5Lku7bkuI7mqKHmnb8g5paH56ugIOWVhuWfjiDlvq7kv6Eg6K665Z2bIOS8muWRmCDor4Torrog5pSv5LuYIOenr+WIhiDlt6XkvZzmtYEg5Lu75Yqh6LCD5bqm562JIOWQjOaXtuaPkOS+m+S4iueZvuWll+WFjei0ueaooeadv+S7u+aEj+mAieaLqSDku7flgLzmupDoh6rliIbkuqsg6ZOt6aOe57O757uf5LiN5LuF5LiA5aWX566A5Y2V5aW955So55qE5byA5rqQ57O757ufIOabtOaYr+S4gOaVtOWll+S8mOi0qOeahOW8gOa6kOeUn+aAgeWGheWuueS9k+ezuyDpk63po57nmoTkvb/lkb3lsLHmmK/pmY3kvY7lvIDlj5HmiJDmnKzmj5Dpq5jlvIDlj5HmlYjnjocg5o+Q5L6b5YWo5pa55L2N55qE5LyB5Lia57qn5byA5Y+R6Kej5Yaz5pa55qGIIOavj+aciDI45a6a5pyf5pu05paw54mI5pysV2VIYWxvIOeugOe6pumjjiDnmoTlvq7kv6HlsI/nqIvluo/niYjljZrlrqIg5Z+65LqOIHZ1ZTIgdnVleCDmnoTlu7rkuIDkuKrlhbfmnIkgNDUg5Liq6aG16Z2i55qE5aSn5Z6L5Y2V6aG16Z2i5bqU55So5Z+65LqOVnVlMyBFbGVtZW50IFBsdXMg55qE5ZCO5Y+w566h55CG57O757uf6Kej5Yaz5pa55qGI5Z+65LqOIHZ1ZSBlbGVtZW50IHVpIOeahOWQjuWPsOeuoeeQhuezu+e7n+mynOS6rueahOmrmOmlseWSjOiJsuW9qSDkuJPms6jop4bop4nnmoTlsI/nqIvluo/nu4Tku7blupMg77iPIOi3qOW5s+WPsOahjOmdouerr+inhumikei1hOa6kOaSreaUvuWZqCDnroDmtIHml6Dlub/lkYog5YWN6LS56auY6aKc5YC8IOWQjuWPsOeuoeeQhuS4u+e6v+eJiOacrOWfuuS6juS4ieiAheW5tuihjOW8gOWPkee7tOaKpCDlkIzml7bmlK/mjIHnlLXohJEg5omL5py6IOW5s+advyDliIfmjaLliIbmlK/mn6XnnIvkuI3lkIznmoR2dWXniYjmnKwgZWxlbWVudCBwbHVz54mI5pys5bey5Y+R5biDIHZ1ZTMgdnVlMyB2dWUgdnVlMyB4IHZ1ZSBqcyDnqIvluo/ml6Dlm73nlYwg5L2G56iL5bqP5ZGY5pyJ5Zu955WMIOS4reWbveWbveWutuWwiuS4peS4jeWuueaMkeihhSDlpoLmnpzmgqjlnKjnibnmrorml7bmnJ8gbWFsbCBhZG1pbiB3ZWLmmK/kuIDkuKrnlLXllYblkI7lj7DnrqHnkIbns7vnu5/nmoTliY3nq6/pobnnm64g5Z+65LqOVnVlIEVsZW1lbnTlrp7njrAg5Li76KaB5YyF5ous5ZWG5ZOB566h55CGIOiuouWNleeuoeeQhiDkvJrlkZjnrqHnkIYg5L+D6ZSA566h55CGIOi/kOiQpeeuoeeQhiDlhoXlrrnnrqHnkIYg57uf6K6h5oql6KGoIOi0ouWKoeeuoeeQhiDmnYPpmZDnrqHnkIYg6K6+572u562J5Yqf6IO9IOS4gOasvuWujOWWhOeahOWuieWFqOivhOS8sOW3peWFtyDmlK/mjIHluLjop4Egd2ViIOWuieWFqOmXrumimOaJq+aPj+WSjOiHquWumuS5iSBwb2Mg5L2/55So5LmL5YmN5Yqh5b+F5YWI6ZiF6K+75paH5qGjVnVl5pWw5o2u5Y+v6KeG5YyW57uE5Lu25bqTIOexu+S8vOmYv+mHjERhdGFWIOWkp+Wxj+aVsOaNruWxleekuiDmj5DkvptTVkfnmoTovrnmoYblj4roo4XppbAg5Zu+6KGoIOawtOS9jeWbviDpo57nur/lm77nrYnnu4Tku7Yg566A5Y2V5piT55SoIOmVv+acn+abtOaWsCBSZWFjdOeJiOW3suWPkeW4gyAgVUnooajljZXorr7orqHlj4rku6PnoIHnlJ/miJDlmajln7rkuo5WdWXnmoTlj6/op4bljJbooajljZXorr7orqHlmagg6K6p6KGo5Y2V5byA5Y+R566A5Y2V6ICM6auY5pWIIOWfuuS6jnZ1ZeeahOmrmOaJqeWxleWcqOe6v+e9kemhteWItuS9nOW5s+WPsCDlj6/oh6rlrprkuYnnu4Tku7Yg5Y+v5re75Yqg6ISa5pysIOWPr+aVsOaNrue7n+iuoSB2dWXlkI7lj7DnrqHnkIbmoYbmnrYg57K+6Ie055qE5LiL5ouJ5Yi35paw5ZKM5LiK5ouJ5Yqg6L29IGpz5qGG5p62IOaUr+aMgXZ1ZSDlroznvo7ov5DooYzkuo7np7vliqjnq6/lkozkuLvmtYFQQ+a1j+iniOWZqCDln7rkuo52dWUyIHZ1ZXggZWxlbWVudCB1aeWQjuWPsOeuoeeQhuezu+e7nyAgVnVlIGpz6auY5Lu/6aW/5LqG5LmI5aSW5Y2WQXBw6K++56iL5rqQ56CBIGNvZGluZyBpbW9vYyBjb20gY2xhc3MgNzQgaHRtbOS6rOS4nOmjjuagvOenu+WKqOerryBWdWUyIFZ1ZTMg57uE5Lu25bqTZWxhZG1pbuWJjeerr+a6kOeggSDpobnnm67ln7rkuo7nmoTliY3lkI7nq6/liIbnprvlkI7lj7DnrqHnkIbns7vnu58g5p2D6ZmQ5o6n5Yi26YeH55SoIFJCQUMg6I+c5Y2V5Yqo5oCB6Lev55Sx6LWE5rqQ6YeH6ZuG56uZ5Zyo57q/5pKt5pS+dVZpZXcgVUkg5pivdW5pIGFwcOeUn+aAgeacgOS8mOengOeahFVJ5qGG5p62IOWFqOmdoueahOe7hOS7tuWSjOS+v+aNt+eahOW3peWFt+S8muiuqeaCqOS/oeaJi+aLiOadpSDlpoLpsbzlvpfmsLRWdWUyIOWFqOWutuahtuS7vyDlvq7kv6FBcHAg6aG555uuIOaUr+aMgeWkmuS6uuWcqOe6v+iBiuWkqeWSjOacuuWZqOS6uuiBiuWkqeWJjeerr3Z1ZSDlkI7nq69rb2Eg5YWo5qCI5byP5byA5Y+RYmlsaWJpbGnpppbpobUgQSBtYWdpY2FsIHZ1ZSBhZG1pbiDorrDlvpdzdGFy5LqS6IGU572R5aSn5Y6C5YaF5o6o5Y+K5aSn5Y6C6Z2i57uP5pW055CGIOW5tuS4lOavj+WkqeS4gOmBk+mdouivlemimOaOqOmAgSDmr4/lpKnkupTliIbpkp8g5Y2K5bm05aSn5Y6C5LitIFZ1ZTMg5YWo5a625qG2IFZhbnQg5pCt5bu65aSn5Z6L5Y2V6aG16Z2i5ZWG5Z+O6aG555uuIOaWsOicguWVhuWfjiBWdWUzIOeJiOacrCDmioDmnK/moIjkuLrln7rkuo5WdWXlvIDlj5HnmoRYTWFsbOWVhuWfjuWJjeWPsOmhtemdoiBQQ+erryBWdWXlhajlrrbmobYgVmFudCDmkK3lu7rlpKflnovljZXpobXpnaLnlLXllYbpobnnm64gZGRidXkgNyBvcmFuZ2UgY27liY3lkI7nq6/liIbnprvmnYPpmZDnrqHnkIbns7vnu58g57K+5Yqb5pyJ6ZmQIOWBnOatoue7tOaKpCDnlKggVnVlIGpzIOW8gOWPkeeahOi3qOS4ieerr+W6lOeUqFByb3RvdHlwaW5nIFRvb2wgRm9yIFZ1ZSBEZXZzIOmAgueUqOS6jlZ1ZeeahOWOn+Wei+W3peWFt+WunuaImOWVhuWfjiAg5Z+65LqOVnVlMiDpq5jku7/lvq7kv6FBcHDnmoTljZXpobXlupTnlKhlbGVjdHJvbui3qOW5s+WPsOmfs+S5kOaSreaUvuWZqCDlj6/mkJznvZHmmJPkupEgUVHpn7PkuZAg6Jm+57Gz6Z+z5LmQIOaUr+aMgVFRIOW+ruWNmiBHaXRodWLnmbvlvZUg5LqR5q2M5Y2VIOaUr+aMgeS4gOmUruWvvOWFpemfs+S5kOW5s+WPsOatjOWNlVRob3JVSee7hOS7tuW6kyDovbvph48g566A5rSB55qE56e75Yqo56uv57uE5Lu25bqTIOe7hOS7tuaWh+aho+WcsOWdgCB0aG9ydWkgY24gZG9jIOacgOi/keabtOaWsOaXtumXtCAyIDIxIDUgMjh1bmkgYXBw5qGG5p625ryU56S656S65L6LIEVsZWN0cm9uIFZ1ZSDku7/nvZHmmJPkupHpn7PkuZB3aW5kb3dz5a6i5oi356uvIOWfuuS6jiBWdWUyIFZ1ZSBDTEkzIOeahOmrmOS7v+e9keaYk+S6kSBtYWMg5a6i5oi356uv5pKt5pS+5ZmoIFBDIE9ubGluZSBNdXNpYyBQbGF5ZXJHaXRIdWIg5rOE6Zyy55uR5o6n57O757ufcGVhciDmoqjlrZAg6L276YeP57qn55qE5Zyo57q/6aG555uuIOS7u+WKoeWNj+S9nOezu+e7nyDov5znqIvlip7lhazljY/kvZzoh6rpgInln7rph5HliqnmiYvmmK/kuIDmrL5DaHJvbWXmianlsZUg55So5p2l5b+r6YCf6I635Y+W5YWz5rOo5Z+66YeR55qE5a6e5pe25pWw5o2uIOafpeeci+iHqumAieWfuumHkeeahOWunuaXtuS8sOWAvOaDheWGteaUr+aMgSBtYXJrZG93biDmuLLmn5PnmoTljZrlrqLliY3lj7DlsZXnpLpWdWUg6Z+z5LmQ5pCc57SiIOaSreaUviBEZW1vIOS4gOS4quWfuuS6jiB2dWUyIHZ1ZTMg55qEIOWkp+i9rOebmCDkuZ3lrqvmoLwg5oq95aWW5o+S5Lu25aWW5ZOBIOaWh+WtlyDlm77niYcg6aKc6ImyIOaMiemSruWdh+WPr+mFjee9riDmlK/mjIHlkIzmraUg5byC5q2l5oq95aWWIOamgueOh+WJjSDlkI7nq6/lj6/mjqcg6Ieq5Yqo5qC55o2uIGRwciDosIPmlbTmuIXmmbDluqbpgILphY3np7vliqjnq68g5Z+65LqOIFZ1ZSDnmoTlnKjnur/pn7PkuZDmkq3mlL7lmaggUEMgT25saW5lIG11c2ljIHBsYXllcue+juWboumlv+S6huWQl+WkluWNlue6ouWMheWkluWNluS8mOaDoOWIuCDlhYjpoobnuqLljIXlho3kuIvljZUg5aSW5Y2W57qi5YyF5LyY5oOg5Yi4IGNwc+WIhuaIkCDliKvkurrpoobnuqLljIXkuIvljZUg5L2g5ou/5L2j6YeRIOiuqOiuuuWmguS9leaehOW7uuS4gOWll+WPr+mdoOeahOWkp+Wei+WIhuW4g+W8j+ezu+e7n+eUqCB2dWUg5YaZ5bCP56iL5bqPIOWfuuS6jiBtcHZ1ZSDmoYbmnrbph43lhpkgd2V1aSDln7rkuo5WdWXmoYbmnrbmnoTlu7rnmoRnaXRodWLmlbDmja7lj6/op4bljJblubPlj7Dkvb/nlKhHaXRIdWIgQVBJIOaQreW7uuS4gOS4quWPr+WKqOaAgeWPkeW4g+aWh+eroOeahOWNmuWuouWPr+inhuWMluaLluaLvee7hOS7tuW6kyBERU1P5Z+65LqO5byA5rqQ57uE5Lu2IEluY2VwdGlvbiBTUUxBZHZpc29yIFNPQVIg55qEU1FM5a6h5qC4IFNRTOS8mOWMlueahFdlYuW5s+WPsOaYvuekuuW9k+WJjee9keermeeahOaJgOacieWPr+eUqFRhbXBlcm1vbmtleeiEmuacrCDkuJPms6hXZWLkuI7nrpfms5Xml6DnvJ3mu5rliqhjb21wb25lbnTnsr7pgJrku6XlpKrlnYog5Lit5paH54mIIOe9kemhteaooeaLn+ahjOmdouWfuuS6jueahOWkmuaooeWdl+WJjeWQjuerr+WIhuemu+eahOWNmuWuoumhueebrue9keaYk+S6kemfs+S5kCBRUemfs+S5kCDlkqrlkpXpn7PkuZAg56ys5LiJ5pa5IHdlYuerryDlj6/mkq3mlL4gdmlwIOS4i+aetuatjOabsiDln7rkuo7mnYPpmZDnrqHnkIbnmoTlkI7lj7DnrqHnkIbns7vnu5/ln7rkuo4gTm9kZSBqcyDnmoTlvIDmupDkuKrkurrljZrlrqLns7vnu58g6YeH55SoIE51eHQgVnVlIFR5cGVTY3JpcHQg5oqA5pyv5qCIICDkuIDmrL7nroDmtIHpq5jmlYjnmoRWdWVQcmVzc+efpeivhueuoeeQhiDljZrlrqIgYmxvZyDkuLvpopjln7rkuo51bmkgYXBw55qEdWnmoYbmnrbln7rkuo5WdWUgVnVleCBpVmlld+eahOeUteWtkOWVhuWfjue9keermSBWdWUyIOWFqOWutuahtiBWYW50IOaQreW7uuWkp+Wei+WNlemhtemdouWVhuWfjumhueebriDmlrDonILllYbln47liY3lkI7nq6/liIbnprvniYjmnKwg5YmN56uvVnVl6aG555uu5rqQ56CB6YW354uXIO+4jyDmnoHlrqLnjL/moqblr7zoiKog54us56uL5byA5Y+R6ICF55qE5a+86Iiq56uZICBWdWUgU3ByaW5nQm9vdCBNeUJhdGlzIOmfs+S5kOe9keermeWfuuS6jiBSYWdlRnJhbWUyIOeahOS4gOasvuWFjei0ueW8gOa6kOeahOWfuuehgOWVhuWfjumUgOWUruWKn+iDveeahOW8gOa6kOW+ruWVhuWfjiDln7rkuo52dWUyIOeahOe9keaYk+S6kemfs+S5kOaSreaUvuWZqCBhcGnmnaXoh6rkuo5OZXRlYXNlQ2xvdWRNdXNpY0FwaSB2MiDkuLrmnIDmlrDniYjmnKznvJblhpnnmoTkuIDlpZflkI7lj7DnrqHnkIbns7vnu5/lhajmoIjlvIDlj5HnjovogIXojaPogIDmiYvmnLrnq6/lrpjnvZHlkoznrqHnkIblkI7lj7Dln7rkuo4gVnVlMyB4IFR5cGVTY3JpcHQg55qE5Zyo57q/5ryU56S65paH56i/5bqU55SoIOWunueOsFBQVOW5u+eBr+eJh+eahOWcqOe6v+e8lui+kSDmvJTnpLog5Z+65LqOdnVlMiDnlJ/mgIHnmoTlkI7lj7DnrqHnkIbns7vnu5/mqKHmnb/lvIDlj5HnmoTlkI7lj7DnrqHnkIbns7vnu58gVmNoYXQg5LuO5aS05Yiw6ISaIOaSuOS4gOS4quekvuS6pOiBiuWkqeezu+e7nyB2dWUgbm9kZSBtb25nb2RiIGg157yW6L6R5Zmo57G75Ly8bWFrYSDmmJPkvIHnp4Ag6LSm5Y+3IOWvhueggSBhZG1pbjk5NiDlhazlj7jlsZXnpLog6K6o6K66VnVlIFNwcmluZyBib2905YmN5ZCO56uv5YiG56a76aG555uuIHdoIHdlYiB3aCBzZXJ2ZXLnmoTljYfnuqfniYgg5Z+65LqOZWxlbWVudCB1aeeahOaVsOaNrumpseWKqOihqOWNlee7hOS7tuWfuuS6jiBHaXRIdWIgQVBJIOW8gOWPkeeahOWbvuW6iuelnuWZqCDlm77niYflpJbpk77kvb/nlKgganNEZWxpdnIg6L+b6KGMIENETiDliqDpgJ8g5YWN5LiL6L29IOWFjeWuieijhSDmiZPlvIDnvZHnq5nljbPlj6/nm7TmjqXkvb/nlKgg5YWN6LS5IOeos+WumiDpq5jmlYggIO+4jyBWdWXliJ0g5Lit57qn6aG555uuIENub2RlSlPnpL7ljLrph43mnoTpooTop4ggREVNTyDln7rkuo52dWUy5YWo5a625qG25a6e546w55qEIOS7v+enu+WKqOerr1FR5Z+65LqOVnVlIEVjaGFydHMg5p6E5bu655qE5pWw5o2u5Y+v6KeG5YyW5bmz5Y+wIOmFt+eCq+Wkp+Wxj+Wxleekuuaooeadv+WSjOe7hOS7tuW6kyDmjIHnu63mm7TmlrDlkITooYzlkITkuJrlrp7nlKjmqKHmnb/lkozngqvphbflsI/nu4Tku7Yg5Z+65LqOU3ByaW5nIEJvb3TnmoTlnKjnur/ogIPor5Xns7vnu58g6aKE6KeI5Zyw5Z2AIDEyOSAyMTEgODggMTkxIOi0puaIt+WIhuWIq+aYr2FkbWluIHRlYWNoZXIgc3R1ZGVudCDlr4bnoIHmmK9hZG1pbjEyMyA2cGFuIDbnm5jlsI/nmb3nvoog56ys5LqM54mIIHZ1ZTMgYW50ZCB0eXBlc2NyaXB0IG9uIGJvbmUgYW5kIGtuaWZlIOWfuuS6jlZ1ZSDlhajlrrbmobYgMiB4IOWItuS9nOeahOe+juWbouWkluWNlkFQUCDmnKzpobnnm67mmK/kuIDmrL7ln7rkuo4gQXZ1ZSDnmoTooajljZXorr7orqHlmagg5ouW5ou95byP5pON5L2c6K6p5L2g5b+r6YCf5p6E5bu65LiA5Liq6KGo5Y2VIOS4gOWIu+ekvuWMuuWJjeerr+a6kOeggeWfuuS6jlZ1ZSBpVmlldyBBZG1pbuW8gOWPkeeahFhCb2905YmN5ZCO56uv5YiG56a75byA5pS+5bmz5Y+w5YmN56uvIOadg+mZkOWPr+aOp+WItuiHs+aMiemSruaYvuekuiDliqjmgIHot6/nlLHmnYPpmZDoj5zljZUg5aSa6K+t6KiAIOeugOa0gee+juingiDliY3lkI7nq6/liIbnprsg77iP5LiA5Liq5byA5rqQ55qE56S+5Yy656iL5bqPIOS4tOaXtua1i+ivleermSBodHRwcyB0IG15cnBnIGNuZWNoYXJ0c+WcsOWbvmdlb0pzb27ooYzmlL/ovrnnlYzmlbDmja7nmoTlrp7ml7bojrflj5bkuI7lupTnlKgg55yB5biC5Yy65Y6/5aSa57qn6IGU5Yqo5LiL6ZK7IOecn+ato+aEj+S5ieeahOS4i+mSu+iHs+WOv+e6pyDpmYTmnIDmlrBnZW9Kc29u5paH5Lu25LiL6L29IFZ1ZeeahE51eHQganPmnI3liqHnq6/muLLmn5PmoYbmnrYgTm9kZUpT5Li65ZCO56uv55qE5YWo5qCI6aG555uuIERvY2tlcuS4gOmUrumDqOe9siDpnaLlkJHlsI/nmb3nmoTlroznvo7ljZrlrqLns7vnu592dWXngJHluIPmtYHnu4Tku7YgdnVlIHdhdGVyZmFsbCBlYXN5IDIgeCBFZ28g56e75Yqo56uv6LSt54mp5ZWG5Z+OIHZ1ZSB2dWV4IHJ1b3RlciB3ZWJwYWNrICBWdWUganMgTm9kZSBqcyBNb25nb2RiIOWJjeWQjuerr+WIhuemu+eahOS4quS6uuWNmuWuouWktOWDj+WKoOWPo+e9qeWwj+eoi+W6jyDln7rkuo51bmlhcHDkvb/nlKh2dWXlv6vpgJ/lrp7njrAg5bm/5ZGK5pyI5pS25YWlNGsgIOWfuuS6jnZ1ZTMg55qE566h55CG56uv5qih5p2/5pWZ5L2g5aaC5L2V5omT6YCg6IiS6YCCIOmrmOaViCDml7blsJrnmoTliY3nq6/lvIDlj5Hnjq/looPln7rkuo4gRmxhc2sg5ZKMIFZ1ZSBqcyDliY3lkI7nq6/liIbnprvnmoTlvq7lnovljZrlrqLpobnnm64g5pSv5oyB5aSa55So5oi3IE1hcmtkb3du5paH56ugIOWWnOasoiDmlLbol4/mlofnq6Ag57KJ5Lid5YWz5rOoIOeUqOaIt+ivhOiuuiDngrnotZ4g5Yqo5oCB6YCa55+lIOermeWGheengeS/oSDpu5HlkI3ljZUg6YKu5Lu25pSv5oyBIOeuoeeQhuWQjuWPsCDmnYPpmZDnrqHnkIYgUlHku7vliqHpmJ/liJcgRWxhc3RpY3NlYXJjaOWFqOaWh+aQnOe0oiBMaW51eCBWUFPpg6jnvbIgRG9ja2Vy5a655Zmo6YOo572y562J5Z+65LqOIHZ1ZSDlkowgaGV5dWkg57uE5Lu25bqT55qE5Lit5ZCO56uv57O757ufIGFkbWluIGhleXVpIHRvcFZ1ZSDovbvph4/nuqflkI7lj7DnrqHnkIbns7vnu5/ln7rnoYDmqKHmnb91bmkgYXBw6aG555uu5o+S5Lu25Yqf6IO96ZuG5ZCIV2Xlt53lpKflsI/nqIvluo8gc2N1cGx1cyDkvb/nlKh3ZXB55byA5Y+R55qE5a6M5ZaE55qE5qCh5Zut57u85ZCI5bCP56iL5bqPIDQg6aG16Z2iIOWJjeWQjuerr+W8gOa6kCDljIXmi6zmiJDnu6kg6K++6KGoIOWkseeJqeaLm+mihiDlm77kuabppoYg5paw6Ze76LWE6K6v562J562J5bi46KeB5qCh5Zut5Zy65pmv5Yqf6IO95LiA5Liq5YWo6ZqP5py655qE5Yi36KOF5aSH5bCP5ri45oiP5LiA5LiqdnVl5YWo5a625qG25YWl6ZeoRGVtbyDmmK/kuIDlgIvlj6/ku6XluavliqnmgqggVnVlIGpzIOeahOmgheebrua4rOippuWPiuWBtemMr+eahOW3peWFtyDkuZ/lkIzmmYLmlK/mjIEgVnVleOWPiiBWdWUgUm91dGVyICDlvq7kv6HlhazkvJflj7fnrqHnkIbns7vnu58g5YyF5ZCr5YWs5LyX5Y+36I+c5Y2V566h55CGIOiHquWKqOWbnuWkjSDntKDmnZDnrqHnkIYg5qih5p2/5raI5oGvIOeyieS4neeuoeeQhiDvuI/nrYnlip/og70g5YmN5ZCO56uv6YO95byA5rqQ5YWN6LS5ICDln7rkuo52dWUg55qE566h55CG5ZCO5Y+wIOmFjeWQiEJsb2cgQ29yZeS4jkJsb2cgVnVl562J5aSa5Liq6aG555uu5L2/55So5rW36aOO5bCP5bqXIOW8gOa6kOWVhuWfjiDlvq7kv6HlsI/nqIvluo/llYbln47nrqHnkIblkI7lj7Ag5ZCO5Y+w566h55CGIFZVRSBJVOS5i+WutuesrOS4ieaWueWwj+eoi+W6j+eJiOWuouaIt+erryDkvb/nlKggbXB2dWUg5byA5Y+RIOWFvOWuuSB3ZWIgdnVlIOWPr+S7peaLluaLveaOkuW6j+eahOagkeW9ouihqOagvCDnjrDku6MgV2ViIOW8gOWPkeivreazleWfuuehgOS4juW3peeoi+Wunui3tSDmtrXnm5YgV2ViIOW8gOWPkeWfuuehgCDliY3nq6/lt6XnqIvljJYg5bqU55So5p625p6EIOaAp+iDveS4juS9k+mqjOS8mOWMliDmt7flkIjlvIDlj5EgUmVhY3Qg5a6e6Le1IFZ1ZSDlrp7ot7UgV2ViQXNzZW1ibHkg562J5aSa5pa56Z2iICDmlbDmja7lpKflsY/lj6/op4bljJbnvJbovpHlmajkuIDkuKrpgILnlKjkuo7mkYTlvbHku47kuJrogIUg54ix5aW96ICFIOiuvuiuoeW4iOetieWIm+aEj+ihjOS4muS7juS4muiAheeahOWbvuWDj+W3peWFt+eusSDmrabmsYnlpKflrablm77kuabppobliqnmiYsg5qGM6Z2i56uv5Z+65LqOZm9ybSBnZW5lcmF0b3Ig5Lu/6ZKJ6ZKJ5a6h5om55rWB56iL5Yib5bu6IOihqOWNleWIm+W7uiDmtYHnqIvoioLngrnlj6/op4bljJbphY3nva4g5b+F5aGr5p2h5Lu25Y+K5qCh6aqMIOS4gOS4quWujOaVtGVsZWN0cm9u5qGM6Z2i6K6w6LSm56iL5bqPIOaKgOacr+agiOS4u+imgeS9v+eUqGVsZWN0cm9uIHZ1ZSB2dWV0aWZ5IOW8gOacuuiHquWKqOWQr+WKqCDoh6rliqjmm7TmlrAg5omY55uY5pyA5bCP5YyWIOmXqueDgeetieW4uOeUqOWKn+iDvSBOc2lz5Yi25L2c5ryC5Lqu55qE5a6J6KOF5YyFIOeoi+W6j+eMv+eahOWpmuekvOmCgOivt+WHvSDkuIDkuKrln7rkuo52dWXlkoxlbGVtZW50IHVp55qE5qCR5b2i56m/5qKt5qGG5Y+K6YKu5Lu26YCa6K6v5b2V54mI5pys6KeB56S65L6L6KeBIOWfuuS6jkdpbiBWdWUgRWxlbWVudCBVSeeahOWJjeWQjuerr+WIhuemu+adg+mZkOeuoeeQhuezu+e7n+eahOWJjeerr+aooeWdl+mAmueUqOS5puexjemYheivu0FQUCBCb29rQ2hhdCDnmoQgdW5pIGFwcCDlrp7njrDniYjmnKwg5pSv5oyB5aSa56uv5YiG5Y+RIOe8luivkeeUn+aIkEFuZHJvaWTlkoxpT1Mg5omL5py6QVBQ5Lul5Y+K5ZCE5bmz5Y+w55qE5bCP56iL5bqP5Z+65LqOVnVlM+eahE1hdGVyaWFsIGRlc2lnbumjjuagvOenu+WKqOerr+e7hOS7tuW6k+i/m+mYtui1hOa3seWJjeerr+W8gOWPkeWcqOe6v+iAg+ivleezu+e7nyBzcHJpbmdib290IHZ1ZeWJjeWQjuerr+WIhuemu+eahOS4gOS4qumhueebriAg77iPIOaXoOWQjuerr+eahOS7vyBZb3VUdWJlIExpdmUgQ2hhdCDpo47moLznmoTnroDmmJMgQmlsaWJpbGkg5by55bmV5aesdnVl5ZCO56uv566h55CG57O757uf55WM6Z2iIOWfuuS6jnVp57uE5Lu2aXZpZXdCaWxpYmlsaeebtOaSreW8ueW5leW6kyBmb3IgTWFjIFdpbmRvd3MgTGludXhWdWXpq5jku7/nvZHmmJPkupHpn7PkuZAg5Z+65pys5a6e546w572R5piT5LqR5omA5pyJ6Z+z5LmQIE1W55u45YWz5Yqf6IO9IOeOsOW3suabtOaWsOWIsOesrOS6jOeJiCDku4XnlKjkuo7lrabkuaAg5LiL6Z2i5pyJ6K+m57uG5pWZ56iLIOatpuaxieWkp+WtpuWbvuS5pummhuWKqeaJiyDnp7vliqjnq69aZXVz5Z+65LqOR29sYW5nIEdpbiBjYXNiaW4g6Ie05Yqb5LqO5YGa5LyB5Lia57uf5LiA5p2D6ZmQIOi0puWPt+S4reW/g+euoeeQhuezu+e7nyDljIXlkKvotKblj7fnrqHnkIYg5pWw5o2u5p2D6ZmQIOWKn+iDveadg+mZkCDlupTnlKjnrqHnkIYg5aSa5pWw5o2u5bqT6YCC6YWNIOWPr2RvY2tlciDkuIDplK7ov5DooYwg56S+5Yy65rS76LeDIOeJiOacrOi/reS7o+W/qyDliqDnvqTlhY3otLnmioDmnK/mlK/mjIEgVnVl6auY5Lu/572R5piT5LqR6Z+z5LmQIFZ1ZeWFpemXqOWunui3tSDlnKjnur/pooTop4gg5pqC5pe25YGc5q2i5Z+65LqOIFZ1ZSDlkowgRWxlbWVudFVJIOaehOW7uueahOS4gOS4quS8geS4mue6p+WQjuWPsOeuoeeQhuezu+e7nyDvuI8g6Leo5bmz5Y+w56e75Yqo56uv6KeG6aKR6LWE5rqQ5pKt5pS+5ZmoIOeugOa0geWFjei0uSBaWSBQbGF5ZXIg56e75Yqo56uvIEFQUCDln7rkuo4gVW5pIGFwcCDlvIDlj5EgVnVl5a6e5oiY6aG555uu5Z+65LqO5Y+C6ICD5bCP57Gz5ZWG5Z+OIOWunueOsOeahOeUteWVhumhueebriBoNeWItuS9nCDnp7vliqjnq6/kuJPpopjmtLvliqjpobXpnaLlj6/op4bljJbnvJbovpHku7/pkonpkonlrqHmibnmtYHnqIvorr7nva7liqjmgIHooajljZXpobXpnaLorr7orqEg6Ieq5Yqo55Sf5oiQ6aG16Z2i5b6u5YmN56uv6aG555uu5a6e5oiYdnVl6aG555uuIOWfuuS6jnZ1ZTMgcWlhbmt1bjIg6L+b6Zi254mIIGdpdGh1YiBjb20gd2wgdWkgd2wgbWZl5Z+65LqOIGQyIGFkbWlu55qEUkJBQ+adg+mZkOeuoeeQhuino+WGs+aWueahiFZ1ZU5vZGUg5piv5LiA5aWX5Z+65LqO55qE5YmN5ZCO56uv5YiG56a76aG555uuIOWfuuS6juS7v+S6rOS4nOa3mOWuneeahCDnp7vliqjnq69INeeUteWVhuW5s+WPsCAg5beo5qCRIOWfuuS6jnp0cmVl5bCB6KOF55qEVnVl5qCR5b2i57uE5Lu2IOi9u+advuWunueOsOa1t+mHj+aVsOaNrueahOmrmOaAp+iDvea4suafkyDlvq7kv6HnuqLljIXlsIHpnaLpooblj5Yg55So5oi36KeC55yL6KeG6aKR5bm/5ZGK5oiW6ICF6YKA6K+355So5oi35Y+v6I635Y+W5b6u5L+h57qi5YyF5bqP5YiX5Y+35Z+65LqOIFZ1ZSDnmoTlj6/op4bljJbluIPlsYDnvJbovpHlmajmj5Lku7ZrYm9uZSB1aSDmmK/kuIDlpZfog73lkIzml7bmlK/mjIEg5bCP56iL5bqPIGtib25lIOWSjCB2dWUg5qGG5p625byA5Y+R55qE5aSa56uvIFVJIOW6kyBQUyDmlrDniYgga2JvbmUgdWkg5bey5Ye654KJ5bm26L+B56e75YiwIGtib25lIOS4u+S7k+W6kyDmraTku5PlupPku4XlgZrml6fniYjnu7TmiqTkuYvnlKggIOS4gOS4qnZ1ZeeahOS4quS6uuWNmuWuoumhueebriDphY3lkIggbmV0IGNvcmUgYXBp5pWZ56iLIOaJk+mAoOWJjeWQjuerr+WIhuemu1R1bW8gQmxvZyBGb3IgVnVlIGpzIOWJjeWQjuerr+WIhuemu2JwbW4ganPmtYHnqIvorr7orqHlmajnu4Tku7Yg5Z+65LqOdnVlIGVsZW1lbnR1aee+juWMluWxnuaAp+mdouadvyDmu6HotrM5ICXku6XkuIrnmoTkuJrliqHpnIDmsYLkuJPpl6jkuLogV2VleCDliY3nq6/lvIDlj5HogIXmiZPpgKDnmoTkuIDlpZfpq5jotKjph49VSeahhuaetiDmg7PnlKh2dWXmiormiJHnjrDlnKjnmoTkuKrkurrnvZHnq5nph43mlrDlhpnkuIDkuIsg5paw55qE6aOO5qC8IOaWsOeahOaKgOacryDku4DkuYjpg73mmK/mlrDnmoQg5pys6aG555uu5piv5LiA5Liq5Zyo57q/6IGK5aSp57O757ufIOacgOWkp+eoi+W6pueahOi/mOWOn+S6hk1hY+WuouaIt+err1FRIHZ1ZSBjbGkzIOWQjuWPsOeuoeeQhuaooeadvyBoZWFydCDln7rkuo52dWUy5ZKMdnVleOeahOWkjeadguWNlemhtemdouW6lOeUqCAyIOmhtemdojUz5LiqQVBJIOS7v+WunumqjOalvCDln7rkuo4gVnVlIEtvYSDnmoQgV2ViRGVza3RvcCDop4bnqpfns7vnu58gSmVlYmFzZeaYr+S4gOasvuWJjeWQjuerr+WIhuemu+eahOW8gOa6kOW8gOWPkeahhuaetiDln7rkuo7lvIDlj5Eg5LiA5aWXU3ByaW5nQm9vdOWQjuWPsCDkuKTlpZfliY3nq6/pobXpnaIg5Y+v5Lul6Ieq55Sx6YCJ5oup5Z+65LqORWxlbWVudFVJ5oiW6ICFQW50RGVzaWdu55qE5YmN56uv55WM6Z2iIOS6jOacn+S8muaVtOWQiHJlYWN05YmN56uv5qGG5p62IEFudCBEZXNpZ24gUmVhY3Qg5Zyo5a6e6ZmF5bqU55So5Lit5bey57uP5L2/55So6L+Z5aWX5qGG5p625byA5Y+R5LqGQ01T572R56uZ57O757ufIOekvuWMuuiuuuWdm+ezu+e7nyDlvq7kv6HlsI/nqIvluo8g5b6u5L+h5pyN5Yqh5Y+3562JIOWQjumdouS8mumAkOatpeaVtOeQhuW8gOa6kCDmnKzpobnnm67kuLvopoHnm67nmoTlnKjkuo7mlbTlkIjkuLvmtYHmioDmnK/moYbmnrYg5a+75om+5bqU55So5pyA5L2z6aG555uu5a6e6Le15pa55qGIIOWunueOsOWPr+ebtOaOpeS9v+eUqOeahOW/q+mAn+W8gOWPkeahhuaetiDkvb/nlKggdnVlIGNsaTMg5pCt5bu655qEdnVlIHZ1ZXggcm91dGVyIGVsZW1lbnQg5byA5Y+R5qih54mIIOmbhuaIkOW4uOeUqOe7hOS7tiDlip/og73mqKHlnZdKRUVDRyBCT09UIEFQUCDnp7vliqjop6PlhrPmlrnmoYgg6YeH55SodW5pYXBw5qGG5p62IOS4gOS7veS7o+eggeWkmue7iOerr+mAgumFjSDlkIzml7bmlK/mjIFBUFAg5bCP56iL5bqPIEg1IOWunueOsOS6huS4jkplZWNnQm9vdOW5s+WPsOWujOe+juWvueaOpeeahOenu+WKqOino+WGs+aWueahiCDnm67liY3lt7Lnu4/lrp7njrDnmbvlvZUg55So5oi35L+h5oGvIOmAmuiur+W9lSDlhazlkYog56e75Yqo6aaW6aG1IOS5neWuq+agvOetieWfuuehgOWKn+iDvSDmmI7ml6XmlrnoiJ/lt6XlhbfnrrEg5pSv5oyB5Lit5Y+w576O5pel6Z+p5pyNdnVl55qE6aqM6K+B56CB5o+S5Lu26L+Z6YeM5pyJ5LiA5Lqb5qCH5YeG57uE5Lu25bqT5Y+v6IO95rKh5pyJ55qE5Yqf6IO957uE5Lu2IOW3suaciee7hOS7tiDmlL7lpKfplZwg562+5YiwIOWbvueJh+agh+etviDmu5Hliqjpqozor4Eg5YCS6K6h5pe2IOawtOWNsCDmi5bmi70g5aSn5a625p2l5om+6IysIOWfuuS6jlZ1ZTIgTm9kZWpzIE15U1FM55qE5Y2a5a6iIOacieWQjuWPsOeuoeeQhuezu+e7nyDmlK/mjIEg55m76ZmGIOazqOWGjCDnlZnoqIAg6K+E6K66IOWbnuWkjSDngrnotZ7plb/msZ/or4HliLjpg5Hlt57lpKflrabotoXluILnrqHnkIbns7vnu5/lpKnlronpl6jlnablhYvmoYzpnaLooYzpqbblt6XlhrXojLbpqazlj6TpgZPph5Hono3mlofmnKzmg4XmhJ/oh6rliqjpu4TmsrPpk7booYzokKXplIDpgJrorrjnq6DmtqZcIixcImh0bWxfdXJsXCI6XCJodHRwczovL2dpdGh1Yi5jb20vY2lyb3NhbnRpbGxpL2NoaW5hLWRpY3RhdG9yc2hpcFwiLFwibGFuZ3VhZ2VcIjpcIkhUTUxcIixcInN0YXJnYXplcnNfY291bnRcIjoyNzE1LFwiZm9ya3NfY291bnRcIjoyNzgsXCJvcGVuX2lzc3Vlc19jb3VudFwiOjgwOCxcInVwZGF0ZWRfYXRcIjpcIjIwMjUtMTEtMTlUMTM6MTE6MTZaXCIsXCJjcmVhdGVkX2F0XCI6XCIyMDE1LTA0LTAyVDIwOjUxOjUwWlwiLFwidG9waWNzXCI6W1wiOTk2XCIsXCJjZW5zb3JzaGlwXCIsXCJjZW5zb3JzaGlwLWNpcmN1bXZlbnRpb25cIixcImNoaW5hXCIsXCJjaGluYS1kaWN0YXRvcnNoaXBcIixcImNoaW5lc2UtY29tbXVuaXN0LXBhcnR5XCIsXCJjb3ZpZC0xOVwiLFwiY292aWQtMTktY2hpbmFcIixcImRpY3RhdG9yXCIsXCJkaWN0YXRvcnNoaXBcIixcImZhbHVuLWdvbmdcIixcImdmd1wiLFwiZ3JlYXQtZmlyZXdhbGxcIixcImh1bWFuLXJpZ2h0c1wiLFwic2hhZG93c29ja3NcIixcInNvY2tzNVwiLFwidGlhbmFubWVuXCIsXCJ0b3RhbGl0YXJpYW5cIixcInhpLWppbnBpbmdcIixcInhpbmppYW5nXCJdLFwicHJpdmF0ZVwiOmZhbHNlLFwiZm9ya1wiOmZhbHNlLFwiYXJjaGl2ZWRcIjpmYWxzZSxcImRlZmF1bHRfYnJhbmNoXCI6XCJtYXN0ZXJcIn0se1wiaWRcIjo5OTc2NDA0NCxcIm5hbWVcIjpcIkFJLUNoaXBcIixcImZ1bGxfbmFtZVwiOlwiYmFzaWNtaS9BSS1DaGlwXCIsXCJkZXNjcmlwdGlvblwiOlwiQSBsaXN0IG9mIElDcyBhbmQgSVBzIGZvciBBSSwgTWFjaGluZSBMZWFybmluZyBhbmQgRGVlcCBMZWFybmluZy5cIixcImh0bWxfdXJsXCI6XCJodHRwczovL2dpdGh1Yi5jb20vYmFzaWNtaS9BSS1DaGlwXCIsXCJsYW5ndWFnZVwiOlwiUEhQXCIsXCJzdGFyZ2F6ZXJzX2NvdW50XCI6MTY5NCxcImZvcmtzX2NvdW50XCI6Mjc4LFwib3Blbl9pc3N1ZXNfY291bnRcIjoyNCxcInVwZGF0ZWRfYXRcIjpcIjIwMjUtMTEtMTVUMTE6Mjk6MTFaXCIsXCJjcmVhdGVkX2F0XCI6XCIyMDE3LTA4LTA5VDA0OjEwOjU0WlwiLFwidG9waWNzXCI6W1wiYWktY2hpcHNcIixcImNoaXBcIixcImRlZXAtbGVhcm5pbmdcIixcIm1hY2hpbmUtbGVhcm5pbmdcIixcInByb2Nlc3NvclwiXSxcInByaXZhdGVcIjpmYWxzZSxcImZvcmtcIjpmYWxzZSxcImFyY2hpdmVkXCI6ZmFsc2UsXCJkZWZhdWx0X2JyYW5jaFwiOlwibWFzdGVyXCJ9LHtcImlkXCI6Mjk3MDAzNzUyLFwibmFtZVwiOlwiLmdpdGh1YlwiLFwiZnVsbF9uYW1lXCI6XCJnZWdlLWNpcmNsZS8uZ2l0aHViXCIsXCJkZXNjcmlwdGlvblwiOlwi6L+Z6YeM5pivR2l0SHVi55qE6I2J5Zy677yM5Lmf5piv5oiI5oiI5ZyI54ix5aW96ICF55qE5Lqk5rWB5Zyw77yM5Li76KaB6K6o6K665Yqo5ryr44CB5ri45oiP44CB56eR5oqA44CB5Lq65paH44CB55Sf5rS7562J5omA5pyJ6K+d6aKY77yM5qyi6L+O5ZCE5L2N5bCP5LyZ5Ly05Lus5Zyo5q2k6K6o6K666Laj5LqL44CCVGhpcyBpcyBHaXRIdWIgZ3Jhc3NsYW5kLCBhbmQgdGhlIGNvbW11bml0eSBwbGFjZSBmb3IgR2VnZSBjaXJjbGUgbG92ZXJzLCBtYWlubHkgZGlzY3Vzc2VzIGFuaW1lLCBnYW1lcywgdGVjaG5vbG9neSwgbGlmaW5nIGFuZCBvdGhlciB0b3BpY3MuIFlvdSBhcmUgd2VsY29tZSB0byBzaGFyZSBpbnRlcmVzdCB0aGluZ3MgaGVyZS4gIOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOOAgOe8uuawp+S/ruaUueWZqCDmjY/ohLgg5Y+N5YWJ5p2/IE15U1FM5pWw5o2u5bqTIOeOieexs+adguiNieaVsOaNrumbhiDplIDllK7ns7vnu5/lvIDlj5Eg55ar5oOF5pyf6Ze0572R5rCR5oOF57uq6K+G5Yir5q+U6LWbIDk5NmljdSDpooTmtYvnu5Pmnpzlr7zlh7og6LWW5Lyf5p6X5Yi65p2A5bCP6K+05a62IOi0reeJqeWVhuWcuiDoi7Hor63or43msYfph4/lsI/nqIvluo8g6IGU57qn6YCJ5oup5ZmoQml0Y29pbuWMuuWdl+mTviDmioDmnK/pnaLor5Xlv4XlpIfln7rnoYDnn6Xor4YgTGVldGNvZGUg6K6h566X5py65pON5L2c57O757ufIOiuoeeul+acuue9kee7nCDns7vnu5/orr7orqEgSmF2YeWtpuS5oCDpnaLor5XmjIfljZcg5LiA5Lu95ra155uW5aSn6YOo5YiGIEphdmEg56iL5bqP5ZGY5omA6ZyA6KaB5o6M5o+h55qE5qC45b+D55+l6K+GIOWHhuWkhyBKYXZhIOmdouivlSDpppbpgIkgSmF2YUd1aWRlIFB5dGhvbiAxIOWkqeS7juaWsOaJi+WIsOWkp+W4iOWIt+eul+azleWFqOmdoOWll+i3ryDorqTlh4YgbGFidWxhZG9uZyDlsLHlpJ/kuoYg5YWN6LS555qE6K6h566X5py657yW56iL57G75Lit5paH5Lmm57GNIOasoui/juaKleeov+eUqOWKqOeUu+eahOW9ouW8j+WRiOeOsOino0xlZXRDb2Rl6aKY55uu55qE5oCd6LevIOS6kuiBlOe9kSBKYXZhIOW3peeoi+W4iOi/m+mYtuefpeivhuWujOWFqOaJq+ebsiDmtrXnm5bpq5jlubblj5Eg5YiG5biD5byPIOmrmOWPr+eUqCDlvq7mnI3liqEg5rW36YeP5pWw5o2u5aSE55CG562J6aKG5Z+f55+l6K+G5ZCO56uv5p625p6E5biI5oqA5pyv5Zu+6LCxbWFsbOmhueebruaYr+S4gOWll+eUteWVhuezu+e7nyDljIXmi6zliY3lj7DllYbln47ns7vnu5/lj4rlkI7lj7DnrqHnkIbns7vnu58g5Z+65LqOU3ByaW5nQm9vdCBNeUJhdGlz5a6e546wIOmHh+eUqERvY2tlcuWuueWZqOWMlumDqOe9siDliY3lj7DllYbln47ns7vnu5/ljIXlkKvpppbpobXpl6jmiLcg5ZWG5ZOB5o6o6I2QIOWVhuWTgeaQnOe0oiDllYblk4HlsZXnpLog6LSt54mp6L2mIOiuouWNlea1geeoiyDkvJrlkZjkuK3lv4Mg5a6i5oi35pyN5YqhIOW4ruWKqeS4reW/g+etieaooeWdlyDlkI7lj7DnrqHnkIbns7vnu5/ljIXlkKvllYblk4HnrqHnkIYg6K6i5Y2V566h55CGIOS8muWRmOeuoeeQhiDkv4PplIDnrqHnkIYg6L+Q6JCl566h55CGIOWGheWuueeuoeeQhiDnu5/orqHmiqXooagg6LSi5Yqh566h55CGIOadg+mZkOeuoeeQhiDorr7nva7nrYnmqKHlnZcg5b6u5L+h5bCP56iL5bqP5byA5Y+R6LWE5rqQ5rGH5oC7IOacgOWFqOS4reWNjuWPpOivl+ivjeaVsOaNruW6kyDllJDlrovkuKTmnJ3ov5HkuIDkuIflm5vljYPlj6Tor5fkurog5o6l6L+RNSA15LiH6aaW5ZSQ6K+X5YqgMjbkuIflrovor5cg5Lik5a6L5pe25pyfMTU2NOS9jeivjeS6uiAyMSA1IOmmluivjSB1bmkgYXBwIOaYr+S9v+eUqCBWdWUg6K+t5rOV5byA5Y+R5bCP56iL5bqPIEg1IEFwcOeahOe7n+S4gOahhuaetjIgMjHlubTmnIDmlrDmgLvnu5Mg6Zi/6YeMIOiFvuiuryDnmb7luqYg576O5ZuiIOWktOadoeetieaKgOacr+mdouivlemimOebriDku6Xlj4rnrZTmoYgg5LiT5a625Ye66aKY5Lq65YiG5p6Q5rGH5oC7IOenkeWtpuS4iue9kSDoh6rnlLHkuIrnvZEg57+75aKZIOi9r+S7tiDmlrnms5Ug5LiA6ZSu57+75aKZ5rWP6KeI5ZmoIOWFjei0uei0puWPtyDoioLngrnliIbkuqsgdnBz5LiA6ZSu5pCt5bu66ISa5pysIOaVmeeoi0FpTGVhcm5pbmcg5py65Zmo5a2m5LmgIE1hY2hpbmVMZWFybmluZyBNTCDmt7HluqblrabkuaAgRGVlcExlYXJuaW5nIERMIOiHqueEtuivreiogOWkhOeQhiBOTFAxMjMgNuaZuuiDveWIt+elqCDorqLnpajlvIDmlL7lvI/ot6jnq6/ot6jmoYbmnrbop6PlhrPmlrnmoYgg5pSv5oyB5L2/55SoIFJlYWN0IFZ1ZSBOZXJ2IOetieahhuaetuadpeW8gOWPkeW+ruS/oSDkuqzkuJwg55m+5bqmIOaUr+S7mOWunSDlrZfoioLot7PliqggUVEg5bCP56iL5bqPIEg1IFJlYWN0IE5hdGl2ZSDnrYnlupTnlKggdGFybyB6b25lIOaOmOmHkee/u+ivkeiuoeWIkiDlj6/og73mmK/kuJbnlYzmnIDlpKfmnIDlpb3nmoToi7Hor5HkuK3mioDmnK/npL7ljLog5pyA5oeC6K+76ICF5ZKM6K+R6ICF55qE57+76K+R5bmz5Y+wIG5vIGV2aWwg56iL5bqP5ZGY5om+5bel5L2c6buR5ZCN5Y2VIOaNouW3peS9nOWSjOW9k+aKgOacr+WQiOS8meS6uumcgOiwqOaFjuWViiDmm7TmlrDmnInotZ4g566X5rOV6Z2i6K+VIOeul+azleefpeivhiDpkojlr7nlsI/nmb3nmoTnrpfms5Xorq3nu4Mg6L+Y5YyF5ousIDEg6Zi/6YeMIOWtl+iKgiDmu7Tmu7Qg55m+56+H5aSn5Y6C6Z2i57uP5rGH5oC7IDIg5Y2D5pys5byA5rqQ55S15a2Q5LmmIDMg55m+5byg5oCd57u05a+85Zu+IOWPs+S+p+adpeS4qiBzdGFyIOWQpyBFbmdsaXNoIHZlcnNpb24gc3VwcG9ydGVkIDk1NSDkuI3liqDnj63nmoTlhazlj7jlkI3ljZUg5bel5L2cIDk1NSB3b3Jr4oCTbGlmZSBiYWxhbmNlIOW3peS9nOS4jueUn+a0u+eahOW5s+ihoSDor4rmlq3liKnlmahBcnRoYXMgVGhlIFdheSB0byBHbyDkuK3mlofor5HmnKwg5Lit5paH5q2j5byP5ZCNIEdvIOWFpemXqOaMh+WNlyBKYXZh6Z2i6K+VIEphdmHlrabkuaDmjIfljZcg5LiA5Lu95ra155uW5aSn6YOo5YiGSmF2Yeeoi+W6j+WRmOaJgOmcgOimgeaOjOaPoeeahOaguOW/g+efpeivhiDmlZnnqIsg5oqA5pyv5qCI56S65L6L5Luj56CBIOW/q+mAn+eugOWNleS4iuaJi+aVmeeoiyAyIDE35bm05Lmw5oi/57uP5Y6G5oC757uT5Ye65p2l55qE5Lmw5oi/6LSt5oi/55+l6K+G5YiG5Lqr57uZ5aSn5a62IOW4jOacm+WvueWkp+WutuacieaJgOW4ruWKqSDkubDmiL/kuI3mmJMg5LiU5Lmw5LiU54+N5oOcaHR0cOS4i+i9veW3peWFtyDln7rkuo5odHRw5Luj55CGIOaUr+aMgeWkmui/nuaOpeWIhuWdl+S4i+i9vSDliqjmiYvlrabmt7HluqblrabkuaAg6Z2i5ZCR5Lit5paH6K+76ICFIOiDvei/kOihjCDlj6/orqjorrog5Lit6Iux5paH54mI6KKr5YWo55CDMTc15omA5aSn5a2m6YeH55So5pWZ5a2mIOmYv+mHjOS6keiuoeeul+W5s+WPsOWboumYn+WHuuWTgSDkuLrnm5HmjqfogIznlJ/nmoTmlbDmja7lupPov57mjqXmsaDnqIvluo/lkZjnroDljobmqKHmnb/ns7vliJcg5YyF5ousUEhQ56iL5bqP5ZGY566A5Y6G5qih5p2/IGlPU+eoi+W6j+WRmOeugOWOhuaooeadvyBBbmRyb2lk56iL5bqP5ZGY566A5Y6G5qih5p2/IFdlYuWJjeerr+eoi+W6j+WRmOeugOWOhuaooeadvyBKYXZh56iL5bqP5ZGY566A5Y6G5qih5p2/IEMgQyDnqIvluo/lkZjnroDljobmqKHmnb8gTm9kZUpT56iL5bqP5ZGY566A5Y6G5qih5p2/IOaetuaehOW4iOeugOWOhuaooeadv+S7peWPiumAmueUqOeoi+W6j+WRmOeugOWOhuaooeadv+mHh+eUqOiHqui6q+aooeWdl+inhOiMg+e8luWGmeeahOWJjeerryBVSSDmoYbmnrYg6YG15b6q5Y6f55SfIEhUTUwgQ1NTIEpTIOeahOS5puWGmeW9ouW8jyDmnoHkvY7pl6jmp5sg5ou/5p2l5Y2z55SoIOi0teagoeivvueoi+i1hOaWmeawkemXtOaVtOeQhiDkvIHkuJrnuqfkvY7ku6PnoIHlubPlj7Ag5YmN5ZCO56uv5YiG56a75p625p6E5by65aSn55qE5Luj56CB55Sf5oiQ5Zmo6K6p5YmN5ZCO56uv5Luj56CB5LiA6ZSu55Sf5oiQIOaXoOmcgOWGmeS7u+S9leS7o+eggSDlvJXpoobmlrDnmoTlvIDlj5HmqKHlvI9PbmxpbmVDb2Rpbmcg5Luj56CB55Sf5oiQIOaJi+W3pU1FUkdFIOW4ruWKqUphdmHpobnnm67op6PlhrM3ICXph43lpI3lt6XkvZwg6K6p5byA5Y+R5pu05YWz5rOo5Lia5YqhIOaXouiDveW/q+mAn+aPkOmrmOaViOeOhyDluK7liqnlhazlj7joioLnnIHmiJDmnKwg5ZCM5pe25Y+I5LiN5aSx54G15rS75oCnIOaIkeaYr+S+neaJrCDmnKjmmJPmnagg5YWs5LyX5Y+3IOmrmOe6p+WJjeerr+i/m+mYtiDkvZzogIUg5q+P5aSp5pCe5a6a5LiA6YGT5YmN56uv5aSn5Y6C6Z2i6K+V6aKYIOelneWkp+WutuWkqeWkqei/m+atpSDkuIDlubTlkI7kvJrnnIvliLDkuI3kuIDmoLfnmoToh6rlt7Eg5Ya057695YaZ5Y2a5a6i55qE5Zyw5pa5IOmihOiuoeWGmeWbm+S4quezu+WIlyBKYXZhU2NyaXB05rex5YWl57O75YiXIEphdmFTY3JpcHTkuJPpopjns7vliJcgRVM257O75YiXIFJlYWN057O75YiXIOS4reaWh+WIhuivjSDor43mgKfmoIfms6gg5ZG95ZCN5a6e5L2T6K+G5YirIOS+neWtmOWPpeazleWIhuaekCDor63kuYnkvp3lrZjliIbmnpAg5paw6K+N5Y+R546wIOWFs+mUruivjeefreivreaPkOWPliDoh6rliqjmkZjopoEg5paH5pys5YiG57G76IGa57G7IOaLvOmfs+eugOe5gei9rOaNoiDoh6rnhLbor63oqIDlpITnkIZmbHV0dGVyIOW8gOWPkeiAheW4ruWKqSBBUFAg5YyF5ZCrIGZsdXR0ZXIg5bi455SoIDE0IOe7hOS7tueahGRlbW8g5ryU56S65LiO5Lit5paH5paH5qGjIOS4i+aLieWIt+aWsCDkuIrmi4nliqDovb0g5LqM57qn5Yi35pawIOa3mOWuneS6jOalvOaZuuiDveS4i+aLieWIt+aWsOahhuaetiDmlK/mjIHotornlYzlm57lvLkg6LaK55WM5ouW5YqoIOWFt+acieaegeW8uueahOaJqeWxleaApyDpm4bmiJDkuoblh6DljYHnp43ngqvphbfnmoRIZWFkZXLlkowgRm9vdGVyIOivpemhueebruW3suaIkOWKn+mbhuaIkCBhY3R1YXRvciDnm5HmjqcgYWRtaW4g5Y+v6KeG5YyW55uR5o6nIGxvZ2JhY2sg5pel5b+XIGFvcExvZyDpgJrov4dBT1DorrDlvZV3ZWLor7fmsYLml6Xlv5cg57uf5LiA5byC5bi45aSE55CGIGpzb27nuqfliKvlkozpobXpnaLnuqfliKsgZnJlZW1hcmtlciDmqKHmnb/lvJXmk44gdGh5bWVsZWFmIOaooeadv+W8leaTjiBCZWV0bCDmqKHmnb/lvJXmk44gRW5qb3kg5qih5p2/5byV5pOOIEpkYmNUZW1wbGF0ZSDpgJrnlKhKREJD5pON5L2c5pWw5o2u5bqTIEpQQSDlvLrlpKfnmoRPUk3moYbmnrYgbXliYXRpcyDlvLrlpKfnmoRPUk3moYbmnrYg6YCa55SoTWFwcGVyIOW/q+mAn+aTjeS9nE15YmF0aXMgUGFnZUhlbHBlciDpgJrnlKjnmoRNeWJhdGlz5YiG6aG15o+S5Lu2IG15YmF0aXMgcGx1cyDlv6vpgJ/mk43kvZxNeWJhdGlzIEJlZXRsU1FMIOW8uuWkp+eahE9STeahhuaetiB1IFB5dGhvbui1hOa6kOWkp+WFqOS4reaWh+eJiCDljIXmi6wgV2Vi5qGG5p62IOe9kee7nOeIrOiZqyDmqKHmnb/lvJXmk44g5pWw5o2u5bqTIOaVsOaNruWPr+inhuWMliDlm77niYflpITnkIbnrYkg55SxIOW8gOa6kOWJjeWTqCDlkowgUHl0aG9u5byA5Y+R6ICFIOW+ruS/oeWFrOWPt+WboumYn+e7tOaKpOabtOaWsCDlkLTmganovr7ogIHluIjnmoTmnLrlmajlrabkuaDor77nqIvkuKrkurrnrJTorrBUbyBCZSBUb3AgSmF2YWVyIEphdmHlt6XnqIvluIjmiJDnpZ7kuYvot6/lvqrluo/muJDov5sg5a2m5Lmg5Y2a5a6iU3ByaW5n57O75YiX5rqQ56CBIG1yYmlyZCBjY+iwouiwouWPr+iDveaYr+iuqeS9oOWPl+ebiuWMqua1heeahOiLseivrei/m+mYtuaMh+WNl+mVnOWDj+e9keaYk+S6kemfs+S5kCBOb2RlIGpzIEFQSSBzZXJ2aWNl5b+r6YCfIOeugOWNlemBv+WFjU9PTeeahGphdmHlpITnkIZFeGNlbOW3peWFt+WfuuS6jiBWdWUganMg55qE5bCP56iL5bqP5byA5Y+R5qGG5p62IOS7juW6leWxguaUr+aMgSBWdWUganMg6K+t5rOV5ZKM5p6E5bu65bel5YW35L2T57O7IOS4reaWh+eJiCBBcHBsZSDlrpjmlrkgU3dpZnQg5pWZ56iL5pys6aG555uu5pu+5Yay5Yiw5YWo55CD56ys5LiAIOW5sui0p+mbhumUpuingeacrOmhtemdouacgOW6lemDqCDlj6blrozmlbTnsr7oh7TnmoTnurjotKjniYgg57yW56iL5LmL5rOVIOmdouivleWSjOeul+azleW/g+W+lyDlt7LlnKjkuqzkuJwg5b2T5b2T5LiK6ZSA5ZSu5aW96IC2IOaYr+Wls+ijhVNlY3VyaXR5IEd1aWRlIGZvciBEZXZlbG9wZXJzIOWunueUqOaAp+W8gOWPkeS6uuWRmOWuieWFqOmhu+efpSDpmL/ph4zlt7Tlt7QgTXlTUUwgYmlubG9nIOWinumHj+iuoumYhSDmtojotLnnu4Tku7YgRUNNQVNjcmlwdCA25YWl6ZeoIOaYr+S4gOacrOW8gOa6kOeahCBKYXZhU2NyaXB0IOivreiogOaVmeeoiyDlhajpnaLku4vnu40gRUNNQVNjcmlwdCA2IOaWsOWinueahOivreazleeJueaApyBDIEMg5oqA5pyv6Z2i6K+V5Z+656GA55+l6K+G5oC757uTIOWMheaLrOivreiogCDnqIvluo/lupMg5pWw5o2u57uT5p6EIOeul+azlSDns7vnu58g572R57ucIOmTvuaOpeijhei9veW6k+etieefpeivhuWPiumdouivlee7j+mqjCDmi5vogZgg5YaF5o6o562J5L+h5oGvIOS4gOasvuS8mOengOeahOW8gOa6kOWNmuWuouWPkeW4g+W6lOeUqCBTb2x1dGlvbnMgdG8gTGVldENvZGUgYnkgR28gMSAlIHRlc3QgY292ZXJhZ2UgcnVudGltZSBiZWF0cyAxICUgTGVldENvZGUg6aKY6Kej5YiG5biD5byP5Lu75Yqh6LCD5bqm5bmz5Y+wWFhMIEpPQiDosLfnspIgQ2hyb21l5o+S5Lu26Iux6ZuE5qacIOS4uuS8mOengOeahENocm9tZeaPkuS7tuWGmeS4gOacrOS4reaWh+ivtOaYjuS5piDorqlDaHJvbWXmj5Lku7boi7Hpm4Tku6zpgKDnpo/kurrnsbvlhazkvJflj7cg5YqgMSDlkIzmraXmm7TmlrDkupLogZTnvZHlhazlj7jmioDmnK/mnrbmnoQg5b6u5L+hIOa3mOWunSDlvq7ljZog6IW+6K6vIOmYv+mHjCDnvo7lm6Lngrnor4Qg55m+5bqmIEdvb2dsZSBGYWNlYm9vayBBbWF6b24gZUJheeeahOaetuaehCDmrKLov45QUuihpeWFhUludGVsbGlKIElERUEg566A5L2T5Lit5paH5LiT6aKY5pWZ56iL56iL5bqP5ZGY5oqA6IO95Zu+6LCx5YmN56uv6Z2i6K+V5q+P5pelIDMgMSDku6XpnaLor5XpopjmnaXpqbHliqjlrabkuaAg5o+Q5YCh5q+P5pel5a2m5Lmg5LiO5oCd6ICDIOavj+Wkqei/m+atpeS4gOeCuSDmr4/lpKnml6nkuIo154K557qv5omL5bel5Y+R5biD6Z2i6K+V6aKYIOatu+ejleiHquW3sSDmhInmgqblpKflrrYgNCDpgZPliY3nq6/pnaLor5XpopjlhajpnaLopobnm5blsI/nqIvluo8g6L2v5oqA6IO9IOWNjuS4uum4v+iSmeaTjeS9nOezu+e7nyDkupLogZTnvZHpppbku73nqIvluo/lkZjogIPlhazmjIfljZcg55SxM+S9jeW3sue7j+i/m+WFpeS9k+WItuWGheeahOWJjeWkp+WOgueoi+W6j+WRmOiBlOWQiOeMruS4iiBNYWPlvq7kv6Hlip/og73mi5PlsZUg5b6u5L+h5o+S5Lu2IOW+ruS/oeWwj+WKqeaJiyBBIHBsdWdpbiBmb3IgTWFjIFdlQ2hhdCDmnLrlmajlrabkuaAg6KW/55Oc5LmmIOWFrOW8j+aOqOWvvOino+aekCDlnKjnur/pmIXor7vlnLDlnYDkuIDmrL7ovbvph4/nuqcg6auY5oCn6IO9IOWKn+iDveW8uuWkp+eahOWGhee9keepv+mAj+S7o+eQhuacjeWKoeWZqCDmlK/mjIF0Y3AgdWRwIHNvY2tzNSBodHRw562J5Yeg5LmO5omA5pyJ5rWB6YeP6L2s5Y+RIOWPr+eUqOadpeiuv+mXruWGhee9kee9keermSDmnKzlnLDmlK/ku5jmjqXlj6PosIPor5Ugc3No6K6/6ZeuIOi/nOeoi+ahjOmdoiDlhoXnvZFkbnPop6PmnpAg5YaF572Rc29ja3M15Luj55CG562J562JIOW5tuW4puacieWKn+iDveW8uuWkp+eahHdlYueuoeeQhuerr+S4gOasvumdouWQkeazm+WJjeerr+S6p+WTgeeglOWPkeWFqOeUn+WRveWRqOacn+eahOaViOeOh+W5s+WPsCDmlofoqIDmlofnt6jnqIvoqp7oqIDmuIXljY7lpKflraborqHnrpfmnLrns7vor77nqIvmlLvnlaXpnaLlkJHkupHljp/nlJ/lvq7mnI3liqHnmoTpq5jlj6/nlKjmtYHmjqfpmLLmiqTnu4Tku7YgT24gSmF2YSA4IOS4reaWh+eJiCDmnKzmlofljp/mlofnlLHnn6XlkI0gSGFja2VyIEVyaWMgUyBSYXltb25kIOaJgOaSsOWvqyDmlZnkvaDlpoLkvZXmraPnorrnmoTmj5Dlh7rmioDooZPllY/poYzkuKbnjbLlvpfkvaDmu7/mhI/nmoTnrZTmoYggUmVhY3QgTmF0aXZl5oyH5Y2X5rGH6ZuG5LqG5ZCE57G7cmVhY3QgbmF0aXZl5a2m5Lmg6LWE5rqQIOW8gOa6kEFwcOWSjOe7hOS7tjEgRGF5cyBPZiBNTCBDb2Rl5Lit5paH54mI5Y2D5Y+k5YmN56uv5Zu+5paH5pWZ56iLIOi2heivpue7hueahOWJjeerr+WFpemXqOWIsOi/m+mYtuWtpuS5oOeslOiusCDku47pm7blvIDlp4vlrabliY3nq68g5YGa5LiA5ZCN57K+6Ie05LyY6ZuF55qE5YmN56uv5bel56iL5biIIOWFrOS8l+WPtyDljYPlj6Tlo7nlj7cg5L2c6ICFIOWfuuS6jiBSZWFjdCDnmoTmuJDov5vlvI/noJTlj5HmoYbmnrYgaWNlIHdvcmvop4bpopHmkq3mlL7lmajmlK/mjIHlvLnluZUg5aSW5oyC5a2X5bmVIOaUr+aMgea7pOmVnCDmsLTljbAgZ2lm5oiq5Zu+IOeJh+WktOW5v+WRiiDkuK3pl7Tlub/lkYog5aSa5Liq5ZCM5pe25pKt5pS+IOaUr+aMgeWfuuacrOeahOaLluWKqCDlo7Dpn7Mg5Lqu5bqm6LCD6IqCIOaUr+aMgei+ueaSrei+uee8k+WtmCDmlK/mjIHop4bpopHoh6rluKZyb3RhdGlvbueahOaXi+i9rCA5IDI3IOS5i+exuyDph43lipvml4vovazkuI7miYvliqjml4vovaznmoTlkIzmraXmlK/mjIEg5pSv5oyB5YiX6KGo5pKt5pS+IOWIl+ihqOWFqOWxj+WKqOeUuyDop4bpopHliqDovb3pgJ/luqYg5YiX6KGo5bCP56qX5Y+j5pSv5oyB5ouW5YqoIOWKqOeUu+aViOaenCDosIPmlbTmr5Tkvosg5aSa5YiG6L6o546H5YiH5o2iIOaUr+aMgeWIh+aNouaSreaUvuWZqCDov5vluqbmnaHlsI/nqpflj6PpooTop4gg5YiX6KGo5YiH5o2i6K+m5oOF6aG16Z2i5peg57yd5pKt5pS+IHJ0c3AgY29uY2F0IG1wZWcgSnVtcFNlcnZlciDmmK/lhajnkIPpppbmrL7lvIDmupDnmoTloKHlnpLmnLog5piv56ym5ZCIIDRBIOeahOS4k+S4mui/kOe7tOWuieWFqOWuoeiuoeezu+e7nyBMaW51eOWRveS7pOWkp+WFqOaQnOe0ouW3peWFtyDlhoXlrrnljIXlkKtMaW51eOWRveS7pOaJi+WGjCDor6bop6Mg5a2m5LmgIOaQnOmbhiBnaXQgaW8gbGludXggYm9vayBOb2RlIGpzIOWMheaVmeS4jeWMheS8miBieSBhbHNvdGFuZ+WPiOS4gOS4quWwj+WVhuWfjiBsaXRlbWFsbCBTcHJpbmcgQm9vdOWQjuerryBWdWXnrqHnkIblkZjliY3nq68g5b6u5L+h5bCP56iL5bqP55So5oi35YmN56uvIFZ1ZeeUqOaIt+enu+WKqOerr+W+ruS/oSDot7PkuIDot7MgUHl0aG9uIOi+heWKqUphdmHotYTmupDlpKflhajkuK3mlofniYgg5YyF5ous5byA5Y+R5bqTIOW8gOWPkeW3peWFtyDnvZHnq5kg5Y2a5a6iIOW+ruS/oSDlvq7ljZrnrYkg55Sx5Lyv5LmQ5Zyo57q/5oyB57ut5pu05pawIHB5dGhvbuaooeaLn+eZu+mZhuS4gOS6m+Wkp+Wei+e9keermSDov5jmnInkuIDkupvnroDljZXnmoTniKzomasg5biM5pyb5a+55L2g5Lus5pyJ5omA5biu5YqpIO+4jyDlpoLmnpzllpzmrKLorrDlvpfnu5nkuKpzdGFy5ZOmIEMg6YKj5Lqb5LqLIOe9kee7nOeIrOiZq+WunuaImCDmt5jlrp0g5Lqs5LicIOe9keaYk+S6kSBC56uZIDEyMyA2IOaKlumfsyDnrJTotqPpmIEg5ryr55S75bCP6K+05LiL6L29IOmfs+S5kOeUteW9seS4i+i9veetiWRlZXBsZWFybmluZyBhaSDlkLTmganovr7ogIHluIjnmoTmt7HluqblrabkuaDor77nqIvnrJTorrDlj4rotYTmupAgU3ByaW5nIEJvb3Tln7rnoYDmlZnnqIsgU3ByaW5nIEJvb3QgMiB454mI5pys6L+e6L295LitIOW4ruWKqSBBbmRyb2lkIEFwcCDov5vooYznu4Tku7bljJbmlLnpgKDnmoTot6/nlLHmoYbmnrYg5pyA5o6l6L+R5Y6f55SfQVBQ5L2T6aqM55qE6auY5oCn6IO95qGG5p625Z+65LqOVnVlMyBFbGVtZW50IFBsdXMg55qE5ZCO5Y+w566h55CG57O757uf6Kej5Yaz5pa55qGI56iL5bqP5ZGY5aaC5L2V5LyY6ZuF55qE5oyj6Zu26Iqx6ZKxIDIg54mIIOWNh+e6p+S4uuWwj+S5puS6hiDku45KYXZh5Z+656GAIEphdmFXZWLln7rnoYDliLDluLjnlKjnmoTmoYbmnrblho3liLDpnaLor5Xpopjpg73mnInlrozmlbTnmoTmlZnnqIsg5Yeg5LmO5ra155uW5LqGSmF2YeWQjuerr+W/heWkh+eahOefpeivhueCuXNwcmluZyBib290IOWunui3teWtpuS5oOahiOS+iyDmmK8gc3ByaW5nIGJvb3Qg5Yid5a2m6ICF5Y+K5qC45b+D5oqA5pyv5bep5Zu655qE5pyA5L2z5a6e6Le1IOWPpuWkluWGmeWNmuWuoiDnlKggT3BlbldyaXRlIOacgOWlveeUqOeahCBWMlJheSDkuIDplK7lronoo4XohJrmnKwg566h55CG6ISa5pys5Lit5Zu956iL5bqP5ZGY5a655piT5Y+R6Z+z6ZSZ6K+v55qE5Y2V6K+NIOe7n+iuoeWtpuS5oOaWueazlSDnmoTku6PnoIHlrp7njrDlhbPkuo5QeXRob27nmoTpnaLor5XpopjmnKzpobnnm67lsIYg5Yqo5omL5a2m5rex5bqm5a2m5LmgIERpdmUgaW50byBEZWVwIExlYXJuaW5nIOWOn+S5puS4reeahE1YTmV05a6e546w5pS55Li6UHlUb3JjaOWunueOsCDmj5Dpq5ggQW5kcm9pZCBVSSDlvIDlj5HmlYjnjofnmoQgVUkg5bqT5YmN56uv57K+6K+75ZGo5YiKIOW4ruS9oOeQhuino+acgOWJjeayvyDlrp7nlKjnmoTmioDmnK8g55qE5aWH5oqA5rer5ben5pe26Ze06YCJ5oup5ZmoIOecgeW4guWMuuS4iee6p+iBlOWKqCBQeXRob27niKzomavku6PnkIZJUOaxoCBwcm94eSBwb29sIExlZXRDb2RlIOWIt+mimOaUu+eVpSAyIOmBk+e7j+WFuOmimOebruWIt+mimOmhuuW6jyDlhbE2IHflrZfnmoTor6bnu4blm77op6Mg6KeG6aKR6Zq+54K55YmW5p6QIDUg5L2Z5byg5oCd57u05a+85Zu+IOS7juatpOeul+azleWtpuS5oOS4jeWGjei/t+iMqyDmnaXnnIvnnIsg5L2g5Lya5Y+R546w55u46KeB5oGo5pmaIOS4gOS4quWfuuS6jiBlbGVjdHJvbiDnmoTpn7PkuZDova/ku7ZGbHV0dGVyIOi2heWujOaVtOeahOW8gOa6kOmhueebriDlip/og73kuLDlr4wg6YCC5ZCI5a2m5Lmg5ZKM5pel5bi45L2/55SoIEdTWUdpdGh1YkFwcOezu+WIl+eahOS8mOWKvyDmiJHku6znm67liY3lt7Lnu4/mi6XmnInlm5vkuKrniYjmnKwg5Yqf6IO96b2Q5YWoIOmhueebruahhuaetuWGheaKgOacr+a2ieWPiumdouW5vyDlrozmiJDluqbpq5gg5oyB57ut57u05oqkIOmFjeWll+aWh+eroCDpgILlkIjlhajpnaLlrabkuaAg5a+55q+U5Y+C6ICDIOi3qOW5s+WPsOeahOW8gOa6kEdpdGh1YuWuouaIt+err0FwcCDmm7Tlpb3nmoTkvZPpqowg5pu05Liw5a+M55qE5Yqf6IO9IOaXqOWcqOabtOWlveeahOaXpeW4uOeuoeeQhuWSjOe7tOaKpOS4quS6ukdpdGh1YiDmj5Dkvpvmm7Tlpb3mm7Tmlrnkvr/nmoTpqb7ovabkvZPpqozOoyDlkIzmrL5XZWV454mI5pys5ZCM5qy+UmVhY3QgTmF0aXZl54mI5pysIGh0dHBzIGcg6L+Z5piv5LiA5Liq55So5LqO5pi+56S65b2T5YmN572R6YCfIENQVeWPiuWGheWtmOWIqeeUqOeOh+eahOahjOmdouaCrOa1rueql+i9r+S7tiDlubbmlK/mjIHku7vliqHmoI/mmL7npLog5pSv5oyB5pu05o2i55qu6IKkIOaYr+S4gOS4qui3qOW5s+WPsOeahOW8uuWKoOWvhuaXoOeJueW+geeahOS7o+eQhui9r+S7tiDpm7bphY3nva4gVjJyYXlVIOWfuuS6jnYycmF55qC45b+D55qEbWFj54mI5a6i5oi356uvIOeUqOS6juenkeWtpuS4iue9kSDkvb/nlKhzd2lmdOe8luWGmSDmlK/mjIF2bWVzcyBzaGFkb3dzb2NrcyBzb2NrczXnrYnmnI3liqHljY/orq4g5pSv5oyB6K6i6ZiFIOaUr+aMgeS6jOe7tOeggSDliarotLTmnb/lr7zlhaUg5omL5Yqo6YWN572uIOS6jOe7tOeggeWIhuS6q+etieeul+azleaooeadvyDmnIDnp5HlrabnmoTliLfpopjmlrnlvI8g5pyA5b+r6YCf55qE5Yi36aKY6Lev5b6EIOS9oOWAvOW+l+aLpeaciSDnu4/lhbjnvJbnqIvkuabnsY3lpKflhagg5ra155uWIOiuoeeul+acuuezu+e7n+S4jue9kee7nCDns7vnu5/mnrbmnoQg566X5rOV5LiO5pWw5o2u57uT5p6EIOWJjeerr+W8gOWPkSDlkI7nq6/lvIDlj5Eg56e75Yqo5byA5Y+RIOaVsOaNruW6kyDmtYvor5Ug6aG555uu5LiO5Zui6ZifIOeoi+W6j+WRmOiBjOS4muS/rueCvCDmsYLogYzpnaLor5XnrYl3YW5nRWRpdG9yIOi9u+mHj+e6p3dlYuWvjOaWh+acrOahhuWJjeerr+i3qOahhuaetui3qOW5s+WPsOahhuaetiDmr4/kuKogSmF2YVNjcmlwdCDlt6XnqIvluIjpg73lupTmh4LnmoQzM+S4quamguW/tSBsZW9uYXJkb21zb+S4gOS4quWPr+S7peingueci+WbveWGheS4u+a1geinhumikeW5s+WPsOaJgOacieinhumikeeahOWuouaIt+err0FuZHJvaWTlvIDlj5HkurrlkZjkuI3lvpfkuI3mlLbpm4bnmoTlt6Xlhbfnsbvpm4blkIgg5pSv5LuY5a6d5pSv5LuYIOW+ruS/oeaUr+S7mCDnu5/kuIDkuIvljZUg5b6u5L+h5YiG5LqrIFppcDRq5Y6L57ypIOaUr+aMgeWIhuWNt+WOi+e8qeS4juWKoOWvhiDkuIDplK7pm4bmiJBVQ3JvcOmAieaLqeWchuW9ouWktOWDjyDkuIDplK7pm4bmiJDkuoznu7TnoIHlkozmnaHlvaLnoIHnmoTmiavmj4/kuI7nlJ/miJAg5bi455SoRGlhbG9nIFdlYlZpZXfnmoTlsIHoo4Xlj6/mkq3mlL7op4bpopEg5Lu/5paX6bG85ruR5Yqo6aqM6K+B56CBIFRvYXN05bCB6KOFIOmch+WKqCBHUFMgTG9jYXRpb27lrprkvY0g5Zu+54mH57yp5pS+IEV4aWYg5Zu+54mH5re75Yqg5Zyw55CG5L2N572u5L+h5oGvIOe7j+e6rOW6piDom5vnvZHnrYnnuqcg6aKc6Imy6YCJ5oup5ZmoIEFyY0dpcyBWVFBLIOe8luivkei/kOihjOS4gOS4i+ivtOS4jeWumuS8muaJvuWIsOaDiuWWnCAxMjMgNiDotK3npajliqnmiYsg5pSv5oyB6ZuG576kIOWkmui0puWPtyDlpJrku7vliqHotK3npajku6Xlj4ogV2ViIOmhtemdoueuoeeQhiBBbmRyb2lk5bm/5ZGK5Zu+54mH6L2u5pKt5o6n5Lu2IOWGhemDqOWfuuS6jlZpZXdQYWdlcjLlrp7njrAgSW5kaWNhdG9y5ZKMVUnpg73lj6/ku6Xoh6rlrprkuYkg6Zu25Luj56CBIOeDreabtOaWsCDoh6rliqjljJYgT1JNIOW6kyDlkI7nq6/mjqXlj6PlkozmlofmoaPpm7bku6PnoIEg5YmN56uvIOWuouaIt+erryDlrprliLbov5Tlm54gSlNPTiDnmoTmlbDmja7lkoznu5PmnoQgTGludXggV2luZG93cyBtYWNPUyDot6jlubPlj7AgVjJSYXkg5a6i5oi356uvIOaUr+aMgeS9v+eUqCBDIFF0IOW8gOWPkSDlj6/mi5PlsZXmj5Lku7blvI/orr7orqEgd2FsbGUg55Om5YqbIERldm9wc+W8gOa6kOmhueebruS7o+eggemDqOe9suW5s+WPsOWfuuS6jiBub2RlIGpzIE1vbmdvZGIg5p6E5bu655qE5ZCO5Y+w57O757ufIGpzIOa6kOeggeino+aekOS4gOS4qua2teebluWFreS4quS4k+agj+WIhuW4g+W8j+a2iOaBr+mYn+WIlyDliIbluIPlvI/kuovliqHnmoTku5PlupMg5biM5pyb6IOW5Y+L5bCP5omL5LiA5oqWIOWPs+S4iuinkuadpeS4qiBTdGFyIOaEn+aBqSAxIDI05Z+65LqOIHZ1ZSBlbGVtZW50IHVpIOeahOWQjuWPsOeuoeeQhuezu+e7n+ejgeWKm+mTvuaOpeiBmuWQiOaQnOe0ouS4reWNjuS6uuawkeWFseWSjOWbveihjOaUv+WMuuWIkiDnnIHnuqcg55yB5Lu955u06L6W5biC6Ieq5rK75Yy6IOWcsOe6pyDln47luIIg5Y6/57qnIOWMuuWOvyDkuaHnuqcg5Lmh6ZWH6KGX6YGTIOadkee6pyDmnZHlp5TkvJrlsYXlp5TkvJog5Lit5Zu955yB5biC5Yy66ZWH5p2R5LqM57qn5LiJ57qn5Zub57qn5LqU57qn6IGU5Yqo5Zyw5Z2A5pWw5o2uIGlPU+W8gOWPkeW4uOeUqOS4ieaWueW6kyDmj5Lku7Yg55+l5ZCN5Y2a5a6i562J562JTGVldENvZGXpopjop6MgMTUx6YGT6aKY5a6M5pW054mI77yP5Lit5paH5paH5qGI5o6S54mI5oyH5YyX5pyA6Imv5b+D55qEIFB5dGhvbiDmlZnnqIsg5Lia5YaF5Li65pWw5LiN5aSa6Ie05Yqb5LqO5p6B6Ie05L2T6aqM55qE6LaF5by65YWo6Ieq56CU6Leo5bmz5Y+wIHdpbmRvd3MgYW5kcm9pZCBpT1Mg5rWB5aqS5L2T5YaF5qC4IOmAmui/h+aooeWdl+WMluiHqueUsee7hOWQiCDmlK/mjIHlrp7ml7ZSVE1Q5o6o5rWBIFJUU1DmjqjmtYEgUlRNUOaSreaUvuWZqCBSVFNQ5pKt5pS+5ZmoIOW9leWDjyDlpJrot6/mtYHlqpLkvZPovazlj5Eg6Z+z6KeG6aKR5a+85pKtIOWKqOaAgeinhumikeWQiOaIkCDpn7PpopHmt7fpn7Mg55u05pKt5LqS5YqoIOWGhee9rui9u+mHj+e6p1JUU1DmnI3liqHnrYkg5q+U5b+r5pu05b+rIOS4mueVjOecn+ato+mdoOiwseeahOi2heS9juW7tui/n+ebtOaSrVNESyAx56eS5YaFIOS9juW7tui/n+aooeW8j+S4izIgNCBtcyDkuIDkuKogUEhQIOW+ruS/oSBTREsg77iPIOi3qOW5s+WPsOahjOmdouerr+inhumikei1hOa6kOaSreaUvuWZqCDnroDmtIHml6Dlub/lkYog5YWN6LS56auY6aKc5YC8IOWQjuWPsOeuoeeQhuS4u+e6v+eJiOacrOWfuuS6juS4ieiAheW5tuihjOW8gOWPkee7tOaKpCDlkIzml7bmlK/mjIHnlLXohJEg5omL5py6IOW5s+advyDliIfmjaLliIbmlK/mn6XnnIvkuI3lkIznmoR2dWXniYjmnKwgZWxlbWVudCBwbHVz54mI5pys5bey5Y+R5biDIHZ1ZTMgdnVlMyB2dWUgdnVlMyB4IHZ1ZSBqcyDnqIvluo/ml6Dlm73nlYwg5L2G56iL5bqP5ZGY5pyJ5Zu955WMIOS4reWbveWbveWutuWwiuS4peS4jeWuueaMkeihhSDlpoLmnpzmgqjlnKjnibnmrorml7bmnJ8g5q2k6aG555uu5piv5py65Zmo5a2m5LmgIE1hY2hpbmUgTGVhcm5pbmcg5rex5bqm5a2m5LmgIERlZXAgTGVhcm5pbmcgTkxQ6Z2i6K+V5Lit5bi46ICD5Yiw55qE55+l6K+G54K55ZKM5Luj56CB5a6e546wIOS5n+aYr+S9nOS4uuS4gOS4queul+azleW3peeoi+W4iOW/heS8mueahOeQhuiuuuWfuuehgOefpeivhiDlpJzor7sg6YCa6L+HIGJpbGliaWxpIOWcqOe6v+ebtOaSreeahOaWueW8j+WIhuS6qyBHbyDnm7jlhbPnmoTmioDmnK/or53popgg5q+P5aSp5aSn5a625Zyo5b6u5L+hIHRlbGVncmFtIFNsYWNrIOS4iuWPiuaXtuayn+mAmuS6pOa1gee8lueoi+aKgOacr+ivnemimCBHaXRIdWJEYWlseSDliIbkuqvlhoXlrrnlrprmnJ/mlbTnkIbkuI7liIbnsbsg5qyi6L+O5o6o6I2QIOiHquiNkOmhueebriDorqnmm7TlpJrkurrnn6XpgZPkvaDnmoTpobnnm64g5pSv5oyB5aSa5a625LqR5a2Y5YKo55qE5LqR55uY57O757uf5py65Zmo5a2m5Lmg55u45YWz5pWZ56iLRGF0YVjmmK/pmL/ph4zkupFEYXRhV29ya3PmlbDmja7pm4bmiJDnmoTlvIDmupDniYjmnKwg6L+Z6YeM5piv5YaZ5Y2a5a6i55qE5Zyw5pa5IEhhbGZyb3N0IEZpZWxkIOWGsOmcnOS5i+WcsG1hbGzlrabkuaDmlZnnqIsg5p625p6EIOS4muWKoSDmioDmnK/opoHngrnlhajmlrnkvY3op6PmnpAgbWFsbOmhueebriA0IGsgc3RhciDmmK/kuIDlpZfnlLXllYbns7vnu58g5L2/55So546w6Zi25q615Li75rWB5oqA5pyv5a6e546wIOa2teebluS6huetieaKgOacryDph4fnlKhEb2NrZXLlrrnlmajljJbpg6jnvbIgY2hpY2sg5piv5L2/55SoIE5vZGUganMg5ZKMIE1vbmdvREIg5byA5Y+R55qE56S+5Yy657O757uf5LiA5Liq6Z2e5bi46YCC5ZCISVTlm6LpmJ/nmoTlnKjnur9BUEnmlofmoaMg5oqA5pyv5paH5qGj5bel5YW35rGH5oC75ZCE5aSn5LqS6IGU572R5YWs5Y+45a655piT6ICD5a+f55qE6auY6aKRbGVldGNvZGXpopggMSBDaGluZXNlIFdvcmQgVmVjdG9ycyDkuIrnmb7np43pooTorq3nu4PkuK3mlofor43lkJHph48gQW5kcm9pZOW8gOa6kOW8ueW5leW8leaTjiDng4jnhLDlvLnluZXkvb8g772e5rex5bqm5a2m5Lmg5qGG5p62UHlUb3JjaCDlhaXpl6jkuI7lrp7miJgg572R5piT5LqR6Z+z5LmQ5ZG95Luk6KGM54mI5pysIOWvueW8gOWPkeS6uuWRmOacieeUqOeahOWumuW+iyDnkIborrog5Y6f5YiZ5ZKM5qih5byPVGVhY2hZb3Vyc2VsZkNTIOeahOS4reaWh+e/u+ivkemrmOminOWAvOeahOesrOS4ieaWuee9keaYk+S6keaSreaUvuWZqCDmlK/mjIEgV2luZG93cyBtYWNPUyBMaW51eCBzcHJpbmcgY2xvdWQgdnVlIG9BdXRoMiDlhajlrrbmobblrp7miJgg5YmN5ZCO56uv5YiG56a75qih5ouf5ZWG5Z+OIOWujOaVtOeahOi0reeJqea1geeoiyDlkI7nq6/ov5DokKXlubPlj7Ag5Y+v5Lul5a6e546w5b+r6YCf5pCt5bu65LyB5Lia57qn5b6u5pyN5Yqh6aG555uuIOaUr+aMgeW+ruS/oeeZu+W9leetieS4ieaWueeZu+W9lSBDaGluZXNlIHN0aWNrZXIgcGFjayBNb3JlIGpveSDooajmg4XljIXnmoTljZrnianppoYgR2l0aHVi5pyA5pyJ5q+S55qE5LuT5bqTIOS4reWbveihqOaDheWMheWkp+mbhuWQiCDogZrmrKLkuZAgTGFudGVybuWumOaWueeJiOacrOS4i+i9vSDok53nga8g57+75aKZIOS7o+eQhiDnp5HlrabkuIrnvZEg5aSW572RIOWKoOmAn+WZqCDmoq/lrZAg6Lev55Sx5LiA5qy+5YWl6Zeo57qn55qE5Lq66IS4IOinhumikSDmloflrZfmo4DmtYvku6Xlj4ror4bliKvnmoTpobnnm64gdnVlMiB2dWUgcm91dGVyIHZ1ZXgg5YWl6Zeo6aG555uuUGFuRG93bmxvYWTnmoTkuKrkurrnu7TmiqTniYjmnKwg5LiA5Liq5Z+65LqOU3ByaW5nIEJvb3QgTXlCYXRpc+eahOenjeWtkOmhueebriDnlKjkuo7lv6vpgJ/mnoTlu7rkuK3lsI/lnotBUEkgUkVTVGZ1bCBBUEnpobnnm64gaU9TIGludGVydmlldyBxdWVzdGlvbnMgaU9T6Z2i6K+V6aKY6ZuG6ZSmIOmZhOetlOahiCDlrabkuaBxcee+pOaIliBUZWxlZ3JhbSDnvqTkuqTmtYHkuLrkupLogZTnvZFJVOS6uuaJk+mAoOeahOS4reaWh+eJiGF3ZXNvbWUgZ2/lvLrlpKcg5Y+v5a6a5Yi2IOaYk+aJqeWxleeahCBWaWV3UGFnZXIg5oyH56S65Zmo5qGG5p62IOaYr+eahOacgOS9s+abv+S7o+WTgSDmlK/mjIHop5LmoIcg5pu05pSv5oyB5Zyo6Z2eVmlld1BhZ2Vy5Zy65pmv5LiL5L2/55SoIOS9v+eUqGhpZGUgc2hvdyDliIfmjaJGcmFnbWVudOaIluS9v+eUqHNlIEt1YmVybmV0ZXPkuK3mlofmjIfljZcg5LqR5Y6f55Sf5bqU55So5p625p6E5a6e6Le15omL5YaMRm9yIG1hY09TIOeZvuW6pue9keebmCDnoLTop6NTVklQIOS4i+i9vemAn+W6pumZkOWItiDmnrbmnoTluIjmioDmnK/lm77osLEg5Yqp5L2g5pep5pel5oiQ5Li65p625p6E5biIbWFsbCBhZG1pbiB3ZWLmmK/kuIDkuKrnlLXllYblkI7lj7DnrqHnkIbns7vnu5/nmoTliY3nq6/pobnnm64g5Z+65LqOVnVlIEVsZW1lbnTlrp7njrAg5Li76KaB5YyF5ous5ZWG5ZOB566h55CGIOiuouWNleeuoeeQhiDkvJrlkZjnrqHnkIYg5L+D6ZSA566h55CGIOi/kOiQpeeuoeeQhiDlhoXlrrnnrqHnkIYg57uf6K6h5oql6KGoIOi0ouWKoeeuoeeQhiDmnYPpmZDnrqHnkIYg6K6+572u562J5Yqf6IO9IOe9keaYk+S6kemfs+S5kOesrOS4ieaWuSDnvJbnqIvpmo/mg7Mg5pW055CG55qEIOWkquWtkOWFmuWFs+ezu+e9kee7nCDkuJPpl6jmj63pnLLotbXlm73nmoTmnYPotLXln7rkuo5naW4gdnVl5pCt5bu655qE5ZCO5Y+w566h55CG57O757uf5qGG5p62IOmbhuaIkGp3dOmJtOadgyDmnYPpmZDnrqHnkIYg5Yqo5oCB6Lev55SxIOWIhumhteWwgeijhSDlpJrngrnnmbvlvZXmi6bmiKog6LWE5rqQ5p2D6ZmQIOS4iuS8oOS4i+i9vSDku6PnoIHnlJ/miJDlmagg6KGo5Y2V55Sf5oiQ5ZmoIOmAmueUqOW3peS9nOa1geetieWfuuehgOWKn+iDvSDkupTliIbpkp/kuIDlpZdDVVJE5YmN5ZCO56uv5Luj56CBIOebrlZVRTPniYjmnKzmraPlnKjph43mnoQg5qyi6L+OaXNzdWXlkoxwciAyN+WkqeaIkOS4ukphdmHlpKfnpZ7kuIDkuKrln7rkuo7mtY/op4jlmajnq68gSlMg5a6e546w55qE5Zyo57q/5Luj55CG57yW56iL55S15a2Q5LmmIOeUteWtkOS5piDnvJbnqIvkuabnsY0g5YyF5ous5Lq65bel5pm66IO9IOWkp+aVsOaNruexuyDlubblj5HnvJbnqIsg5pWw5o2u5bqT57G7IOaVsOaNruaMluaOmCDmlrDpnaLor5Xpopgg5p625p6E6K6+6K6hIOeul+azleezu+WIlyDorqHnrpfmnLrnsbsg6K6+6K6h5qih5byPIOi9r+S7tua1i+ivlSDph43mnoTkvJjljJYg562J5pu05aSa5YiG57G7QURCIFVzYWdlIENvbXBsZXRlIEFEQiDnlKjms5XlpKflhajkuoznu7TnoIHnlJ/miJDlmagg5pSv5oyBIGdpZiDliqjmgIHlm77niYfkuoznu7TnoIEgVmltIOS7juWFpemXqOWIsOeyvumAmumYv+W4g+mHj+WMluS6pOaYk+ezu+e7nyDogqHnpagg5pyf5p2DIOacn+i0pyDmr5TnibnluIEg5py65Zmo5a2m5LmgIOWfuuS6jnB5dGhvbueahOW8gOa6kOmHj+WMluS6pOaYkyDph4/ljJbmipXotYTmnrbmnoTkuIDkuKrnroDmtIHkvJjpm4XnmoRoZXhv5Li76aKYIFdpa2kgb2YgT0kgSUNQQyBmb3IgZXZlcnlvbmUg5p+Q5aSn5Z6L5ri45oiP57q/5LiK5pS755WlIOWGheWQq+eCq+mFt+eul+acr+mtlOazlSBHb29nbGUg5byA5rqQ6aG555uu6aOO5qC85oyH5Y2XIOS4reaWh+eJiCBHaXQgQVdTIEdvb2dsZSDplZzlg48gU1MgU1NSIFZNRVNT6IqC54K56KGM5Lia56CU56m25oql5ZGK55qE55+l6K+G5YKo5aSH5bqTIGNpbSBjcm9zcyBJTSDpgILnlKjkuo7lvIDlj5HogIXnmoTliIbluIPlvI/ljbPml7bpgJrorq/ns7vnu5/lvq7kv6HlsI/nqIvluo/lvIDmupDpobnnm67lupPmsYfmgLvmr4/lpKnmm7TmlrAg5YWo572R54Ot6ZeoIEJUIFRyYWNrZXIg5YiX6KGoIOWkqeeUqEdv5Yqo5omL5YaZIOS7jumbtuWunueOsOezu+WIl+W8uuWkp+eahOWTlOWTqeWTlOWTqeWinuW8uuiEmuacrCDkuIvovb3op4bpopEg6Z+z5LmQIOWwgemdoiDlvLnluZUg566A5YyW55u05pKt6Ze0IOivhOiuuuWMuiDpppbpobUg6Ieq5a6a5LmJ6aG25qCPIOWIoOmZpOW5v+WRiiDlpJzpl7TmqKHlvI8g6Kem5bGP6K6+5aSH5pSv5oyBRXZpbCBIdWF3ZWkg5Y2O5Li65L2c6L+H55qE5oG2QW5kcm9pZOS4iuS4gOS4quS8mOmbhSDkuIfog73oh6rlrprkuYlVSSDku79pT1Mg5pSv5oyB5Z6C55u0IOawtOW5s+aWueWQkeWIh+aNoiDmlK/mjIHlkajop4blm74g6Ieq5a6a5LmJ5ZGo6LW35aeLIOaAp+iDvemrmOaViOeahOaXpeWOhuaOp+S7tiDmlK/mjIHng63mj5Lmi5Tlrp7njrDnmoRVSeWumuWItiDmlK/mjIHmoIforrAg6Ieq5a6a5LmJ6aKc6ImyIOWGnOWOhiDoh6rlrprkuYnmnIjop4blm77lkITnp43mmL7npLrmqKHlvI/nrYkgQ2FudmFz57uY5Yi2IOmAn+W6puW/qyDljaDnlKjlhoXlrZjkvY4g5L2g55yf55qE5oOz5LiN5Yiw5pel5Y6G5bGF54S26L+Y5Y+v5Lul5aaC5q2k5LyY6ZuF5bey5LiN5YaN57u05oqk56eR5a2m5LiK572R5o+S5Lu255qE56a757q/5a6J6KOF5YyF5YKo5a2Y5Zyo6L+Z6YeMVGhpbmtQSFAgRnJhbWV3b3JrIOWNgeW5tOWMoOW/g+eahOmrmOaAp+iDvVBIUOahhuaetiBKYXZhIOeoi+W6j+WRmOecvOS4reeahCBMaW51eCDkuIDkuKrmlK/mjIHlpJrpgIkg6YCJ5Y6f5Zu+5ZKM6KeG6aKR55qE5Zu+54mH6YCJ5oup5ZmoIOWQjOaXtuaciemihOiniCDoo4Hliarlip/og70g5pSv5oyBaHN3ZWIgaGHKinMgd8mbYiDmmK/kuIDkuKrln7rkuo5zcHJpbmcgYm9vdCAyIHjlvIDlj5Eg6aaW5Liq5L2/55So5YWo5ZON5bqU5byP57yW56iL55qE5LyB5Lia57qn5ZCO5Y+w566h55CG57O757uf5Z+656GA6aG555uuIOiHquWKqOWtpuS5oHd4UGFyc2Ug5b6u5L+h5bCP56iL5bqP5a+M5paH5pys6Kej5p6Q6Ieq5a6a5LmJ57uE5Lu2IOaUr+aMgUhUTUzlj4ptYXJrZG93buino+aekCBuZXdiZWUgbWFsbCDpobnnm64g5paw6JyC5ZWG5Z+OIOaYr+S4gOWll+eUteWVhuezu+e7nyDljIXmi6wgbmV3YmVlIG1hbGwg5ZWG5Z+O57O757uf5Y+KIG5ld2JlZSBtYWxsIGFkbWluIOWVhuWfjuWQjuWPsOeuoeeQhuezu+e7nyDln7rkuo4gU3ByaW5nIEJvb3QgMiBYIOWPiuebuOWFs+aKgOacr+agiOW8gOWPkSDliY3lj7DllYbln47ns7vnu5/ljIXlkKvpppbpobXpl6jmiLcg5ZWG5ZOB5YiG57G7IOaWsOWTgeS4iue6vyDpppbpobXova7mkq0g5ZWG5ZOB5o6o6I2QIOWVhuWTgeaQnOe0oiDllYblk4HlsZXnpLog6LSt54mp6L2mIOiuouWNlee7k+eulyDorqLljZXmtYHnqIsg5Liq5Lq66K6i5Y2V566h55CGIOS8muWRmOS4reW/gyDluK7liqnkuK3lv4PnrYnmqKHlnZcg5ZCO5Y+w566h55CG57O757uf5YyF5ZCr5pWw5o2u6Z2i5p2/IOi9ruaSreWbvueuoeeQhiDllYblk4HnrqHnkIYg6K6i5Y2V566h55CGIOS8muWRmOeuoeeQhiDliIbnsbvnrqHnkIYg6K6+572u562J5qih5Z2XIOacgOWFqOeahOWJjeerr+i1hOa6kOaxh+aAu+S7k+W6kyDljIXmi6zliY3nq6/lrabkuaAg5byA5Y+R6LWE5rqQIOaxguiBjOmdouivleetiSDkuK3mlofnv7vor5HmiYvlhpnlrp7njrDmnY7oiKog57uf6K6h5a2m5Lmg5pa55rOVIOS5puS4reWFqOmDqOeul+azlSBQeXRob24g5oqW6Z+z5py65Zmo5Lq6IOiuuuWmguS9leWcqOaKlumfs+S4iuaJvuWIsOa8guS6ruWwj+WnkOWnkO+8nyDvuI9BIHN0YXRpYyBibG9nIHdyaXRpbmcgY2xpZW50IOS4gOS4qumdmeaAgeWNmuWuouWGmeS9nOWuouaIt+erryDotoXnuqfpgJ/mn6Xooagg57yW56iL6K+t6KiAIOahhuaetuWSjOW8gOWPkeW3peWFt+eahOmAn+afpeihqCDljZXkuKrmlofku7bljIXlkKvkuIDliIfkvaDpnIDopoHnn6XpgZPnmoTkuJzopb8g6L+B56e75a2m5Lmg5YmN56uv5L2O5Luj56CB5qGG5p62IOmAmui/hyBKU09OIOmFjee9ruWwseiDveeUn+aIkOWQhOenjemhtemdoiDmioDmnK/pnaLor5XmnIDlkI7lj43pl67pnaLor5XlrpjnmoTor51NYWNoaW5lIExlYXJuaW5nIFllYXJuaW5nIOS4reaWh+eJiCDmnLrlmajlrabkuaDorq3nu4Pnp5jnsY0gQW5kcmV3IE5nIOiRl+i2iuadpei2iuWkmueahOe9keermeWFt+acieWPjeeIrOiZq+eJueaApyDmnInnmoTnlKjlm77niYfpmpDol4/lhbPplK7mlbDmja4g5pyJ55qE5L2/55So5Y+N5Lq657G755qE6aqM6K+B56CBIOW7uueri+WPjeWPjeeIrOiZq+eahOS7o+eggeS7k+W6kyDpgJrov4fkuI7kuI3lkIznibnmgKfnmoTnvZHnq5nlgZrmlpfkuokg5peg5oG25oSPIOaPkOmrmOaKgOacryDmrKLov47mj5DkuqTpmr7ku6Xph4fpm4bnmoTnvZHnq5kg5Zug5bel5L2c5Y6f5ZugIOmhueebruaaguWBnCDmnKzpobnnm67mlLbol4/ov5nkupvlubTmnaXnnIvov4fmiJbogIXlkKzov4fnmoTkuIDkupvkuI3plJnnmoTluLjnlKjnmoTkuIrljYPmnKzkuabnsY0g5rKh5YeG5L2g5oOz5om+55qE5Lmm5bCx5Zyo6L+Z6YeM5ZGiIOWMheWQq+S6huS6kuiBlOe9keihjOS4muWkp+WkmuaVsOS5puexjeWSjOmdouivlee7j+mqjOmimOebruetieetiSDmnInkurrlt6Xmmbrog73ns7vliJcg5bi455So5rex5bqm5a2m5Lmg5qGG5p62VGVuc29yRmxvdyBweXRvcmNoIGtlcmFzIE5MUCDmnLrlmajlrabkuaAg5rex5bqm5a2m5Lmg562J562JIOWkp+aVsOaNruezu+WIlyBTcGFyayBIYWRvb3AgU2NhbGEga2Fma2HnrYkg56iL5bqP5ZGY5b+F5L+u57O75YiXIEMgQyBqYXZhIOaVsOaNrue7k+aehCBsaW51eCDorr7orqHmqKHlvI8g5pWw5o2u5bqT562J562JIOS6uuS6uuW9seinhmJvdCDlrozlhajlr7nmjqXkurrkurrlvbHop4blhajpg6jml6DliKDlh4/otYTmupBTcHJpbmcgQ2xvdWTln7rnoYDmlZnnqIsg5oyB57ut6L+e6L295pu05paw5Lit5LiA5Liq55So5LqO5ZyoIG1hY09TIOS4iuW5s+a7keS9oOeahOm8oOagh+a7muWKqOaViOaenOaIluWNleeLrOiuvue9rua7muWKqOaWueWQkeeahOWwj+W3peWFtyDorqnkvaDnmoTmu5rova7niL3lpoLop6bmjqfmnb/pmL/ph4zlpojlpojliY3nq6/lm6LpmJ/lh7rlk4HnmoTlvIDmupDmjqXlj6PnrqHnkIblt6XlhbdSQVDnrKzkuozku6PotoXovbvph4/nuqfkuK3mlodvY3Ig5pSv5oyB56uW5o6S5paH5a2X6K+G5YirIOaUr+aMgW5jbm4gbW5uIHRubuaOqOeQhuaAu+aooeWei+S7hTQgN00g5b6u5L+h5YWo5bmz5Y+wIFNESyBTZW5wYXJjIFdlaXhpbiBmb3IgQyDmlK/mjIEgTkVUIEZyYW1ld29yayDlj4ogTkVUIENvcmUgTkVUIDYg5bey5pSv5oyB5b6u5L+h5YWs5LyX5Y+3IOWwj+eoi+W6jyDlsI/muLjmiI8g5LyB5Lia5Y+3IOS8geS4muW+ruS/oSDlvIDmlL7lubPlj7Ag5b6u5L+h5pSv5LuYIEpTU0RLIOW+ruS/oeWRqOi+ueetieWFqOW5s+WPsCBXZUNoYXQgU0RLIGZvciBDIOS4reaWh+eLrOeri+WNmuWuouWIl+ihqOmrmOaViOeOhyBRUSDmnLrlmajkurrmlK/mjIHlupPmlK/mjIHlrprliLbku7vkvZXmkq3mlL7lmahTREvlkozmjqfliLblsYIgT3BlblBvd2Vy5bel5L2c57uE5pS26ZuG5rGH5oC755qE5Yy76Zmi5byA5pS+5pWw5o2uWHJheSDln7rkuo4gTmdpbngg55qEIFZMRVNTIFhUTFMg5LiA6ZSu5a6J6KOF6ISa5pysIEZsdXR0ZXJEZW1v5ZCI6ZuGIOS7iuWkqeS9oGZ15LqG5ZCX6I6r54OmUHl0aG9uIOS4reaWh0FJ5pWZ5a2m5Lit5Zu954m56ImyIFRhYkJhciDkuIDooYzku6PnoIHlrp7njrAgTG90dGllIOWKqOeUu1RhYkJhciDmlK/mjIHkuK3pl7TluKYg5Y+355qEVGFiQmFy5qC35byPIOiHquW4pue6oueCueinkuaghyDmlK/mjIHliqjmgIHliLfmlrAgRmx1dHRlcuixhueTo+WuouaIt+erryBBd2Vzb21lIEZsdXR0ZXIgUHJvamVjdCDlhajnvZHmnIAxICXov5jljp/osYbnk6PlrqLmiLfnq68g6aaW6aG1IOS5puW9semfsyDlsI/nu4Qg5biC6ZuG5Y+K5Liq5Lq65Lit5b+DIOS4gOS4quS4jeaLiSBpbWcgeHV2aXAgdG9wIGRvdXlhZGVtbyBtcDQg5Z+65LqOU3ByaW5nQ2xvdWQyIDHnmoTlvq7mnI3liqHlvIDlj5HohJrmiYvmnrYg5pW05ZCI5LqG562JIOacjeWKoeayu+eQhuaWuemdouW8leWFpeetiSDorqnpobnnm67lvIDlj5Hlv6vpgJ/ov5vlhaXkuJrliqHlvIDlj5Eg6ICM5LiN6ZyA6L+H5aSa5pe26Ze06Iqx6LS55Zyo5p625p6E5pCt5bu65LiKIOaMgee7reabtOaWsOS4reWfuuS6jiBWdWUyIOWSjCBFQ2hhcnRzIOWwgeijheeahOWbvuihqOe7hOS7tiBTU1Ig5Y675bm/5ZGKQUNM6KeE5YiZIFNT5a6M5pW0R0ZXTGlzdOinhOWImSBDbGFzaOinhOWImeeijueJhyBUZWxlZ3JhbemikemBk+iuoumYheWcsOWdgOWSjOaIkeS4gOatpeatpemDqOe9siBrdWJlcm5ldGVzIOmbhue+pOaQnOmbhiDmlbTnkIYg57u05oqk5a6e55So6KeE5YiZIOS4reaWh+iHqueEtuivreiogOWkhOeQhuebuOWFs+i1hOaWmeWfuuS6jlNPQeaetuaehOeahOWIhuW4g+W8j+eUteWVhui0reeJqeWVhuWfjiDliY3lkI7nq6/liIbnprsg5YmN5Y+w5ZWG5Z+OIOWFqOWutuahtiDlkI7lj7DnrqHnkIbns7vnu5/nrYlXaGF0IGhhcHBlbnMgd2hlbiDnmoTkuK3mlofnv7vor5Eg5Y6f5LuT5bqTUU1VSSBpT1Mg6Ie05Yqb5LqO5o+Q6auY6aG555uuIFVJIOW8gOWPkeaViOeOh+eahOino+WGs+aWueahiOaWsOWei+WGoOeKtueXheavkumYsueWq+S/oeaBr+aUtumbhuW5s+WPsOWRiuWIq+aer+eHpSDoh7Tlipvkuo7miZPpgKAgUHl0aG9uIOWunueUqOWwj+S+i+WtkOWcqOe6v+WItuS9nCBzb3JyeSDkuLrmiYDmrLLkuLog55qEZ2lmTm9kZWpz5a2m5Lmg56yU6K6w5Lul5Y+K57uP6aqM5oC757uTIOWFrOS8l+WPtyDnqIvluo/njL/lsI/ljaEg5p2O5a6P5q+FIOacuuWZqOWtpuS5oCDnrJTorrAg5Zyo57q/6ZiF6K+75Zyw5Z2AIFZ1ZSBqcyDmupDnoIHliIbmnpBW6YOo6JC9IFZ1ZSBTcHJpbmdCb2905a6e546w55qE5aSa55So5oi35Y2a5a6i566h55CG5bmz5Y+wIEFuZHJvaWQgU2lnbmF0dXJlIFYyIFNjaGVtZeetvuWQjeS4i+eahOaWsOS4gOS7o+a4oOmBk+WMheaJk+WMheelnuWZqEF1dG9zY3JvbGwgQmFubmVyIOaXoOmZkOW+queOr+WbvueJhyDmloflrZfova7mkq3lmagg5aSa56eN57yW56iL6K+t6KiA5a6e546wIExlZXRDb2RlIOWJkeaMhyBPZmZlciDnrKwgMiDniYgg56iL5bqP5ZGY6Z2i6K+V6YeR5YW4IOesrCA2IOeJiCDpopjop6PkuIDlpZfpq5jotKjph4/nmoTlvq7kv6HlsI/nqIvluo8gVUkg57uE5Lu25bqT6aOe5qGoIOWumOaWueaooeWei+W6kyDljIXlkKvlpJrnp43lrabmnK/liY3msr/lkozlt6XkuJrlnLrmma/pqozor4HnmoTmt7HluqblrabkuaDmqKHlnosg5Lit5paHIFB5dGhvbiDnrJTorrDkuJPpl6jkuLrliJrlvIDlp4vliLfpopjnmoTlkIzlrablh4blpIfnmoTnrpfms5Xln7rlnLAg5rKh5pyJ5pyA57uG5Y+q5pyJ5pu057uGIOeri+W/l+eUqOWKqOeUu+WwhuaZpua2qemavuaHgueahOeul+azleivtOeahOmAmuS/l+aYk+aHgiDniYjlhaXpl6jlrp7kvovku6PnoIEg5a6e5oiY5pWZ56iLIOaYr+S4gOS4qumrmOaAp+iDveS4lOS9juaNn+iAl+eahCBnb3JvdXRpbmUg5rGgIENWUFIgMiAyMSDorrrmloflkozlvIDmupDpobnnm67lkIjpm4bmnIkg5pyJIFB5dGhvbui/m+mYtiBJbnRlcm1lZGlhdGUgUHl0aG9uIOS4reaWh+eJiCDmnLrlmajkurrop4bop4kg56e75Yqo5py65Zmo5Lq6IFZTIFNMQU0gT1JCIFNMQU0yIOa3seW6puWtpuS5oOebruagh+ajgOa1iyB5b2xvdjMg6KGM5Li65qOA5rWLIG9wZW5jdiBQQ0wg5py65Zmo5a2m5LmgIOaXoOS6uumpvumptuWQjuWPsOeuoeeQhuezu+e7n+ino+WGs+aWueahiOWIm+W7uuWcqOe6v+ivvueoiyDlrabmnK/nroDljobmiJbliJ3liJvnvZHnq5kgQ2hyb21l5o+S5Lu25byA5Y+R5YWo5pS755WlIOmFjeWll+WujOaVtERlbW8g5qyi6L+OY2xvbmXkvZPpqoxRVUFOVEFYSVMg5pSv5oyB5Lu75Yqh6LCD5bqmIOWIhuW4g+W8j+mDqOe9sueahCDogqHnpagg5pyf6LSnIOacn+adgyDmuK/ogqEg6Jma5ouf6LSn5biBIOaVsOaNriDlm57mtYsg5qih5oufIOS6pOaYkyDlj6/op4bljJYg5aSa6LSm5oi3IOe6r+acrOWcsOmHj+WMluino+WGs+aWueahiOW+ruS/oeiwg+ivlSDlkITnp41XZWJWaWV35qC35byP6LCD6K+VIOaJi+acuua1j+iniOWZqOeahOmhtemdouecn+acuuiwg+ivlSDkvr/mjbfnmoTov5znqIvosIPor5XmiYvmnLrpobXpnaIg5oqT5YyF5bel5YW3IOaUr+aMgSBIVFRQUyDml6DpnIBVU0Lov57mjqXorr7lpIcgcmljaCB0ZXh0IOWvjOaWh+acrOe8lui+keWZqCDmsYnlrZfmi7zpn7MgaMOgbiB6w6wgcMSrbiB5xKtu6Z2i5ZCR5byA5Y+R5Lq65ZGY5qKz55CG55qE5Luj56CB5a6J5YWo5oyH5Y2X5Lul5pK45Luj56CB55qE5b2i5byP5a2m5LmgUHl0aG9u5o+Q5L6b5ZCM6Iqx6aG65a6i5oi356uvIOWbvemHkSDljY7ms7DlrqLmiLfnq68g6Zuq55CD55qE5Z+66YeRIOiCoeelqOiHquWKqOeoi+W6j+WMluS6pOaYk+S7peWPiuiHquWKqOaJk+aWsCDmlK/mjIHot5/ouKogam9pbnF1YW50IHJpY2VxdWFudCDmqKHmi5/kuqTmmJMg5ZKMIOWunuebmOmbqueQg+e7hOWQiCDph4/ljJbkuqTmmJPnu4Tku7bmkJzni5Dop4bpopEgc29odSB0diBSZWRpc+engeacieS6keW5s+WPsHNwcmluZyBib2905omT6YCg5paH5Lu25paH5qGj5Zyo57q/6aKE6KeI6aG555uu6K6h566X5py65Z+656GAIOiuoeeul+acuue9kee7nCDmk43kvZzns7vnu58g5pWw5o2u5bqTIEdpdCDpnaLor5Xpl67popjlhajpnaLmgLvnu5Mg5YyF5ZCr6K+m57uG55qEZm9sbG93IHVwIHF1ZXN0aW9u5Lul5Y+K562U5qGIIOWFqOmDqOmHh+eUqCDpl67popgg6L+96ZeuIOetlOahiCDnmoTlvaLlvI8g5Y2z5ou/5Y2z55SoIOebtOWHu+S6kuiBlOe9keWkp+WOgumdouivlSDlj6/nlKjkuo7mqKHmi5/pnaLor5Ug6Z2i6K+V5YmN5aSN5LmgIOefreacn+WGheW/q+mAn+Wkh+aImOmdouivlSDpppbmrL7lvq7kv6EgbWFjT1Mg5a6i5oi356uv5pKk5Zue5oum5oiq5LiO5aSa5byAd2luZG93cyBrZXJuZWwgZXhwbG9pdHMgV2luZG93c+W5s+WPsOaPkOadg+a8j+a0numbhuWQiOadg+mZkOeuoeeQhuezu+e7nyDpooTop4jlnLDlnYAgNDcgMSA0IDcgMTM4IGxvZ2lucGt1c2Vn5aSa6aKG5Z+f5Lit5paH5YiG6K+N5bel5YW35LiA5qy+5a6M5ZaE55qE5a6J5YWo6K+E5Lyw5bel5YW3IOaUr+aMgeW4uOingSB3ZWIg5a6J5YWo6Zeu6aKY5omr5o+P5ZKM6Ieq5a6a5LmJIHBvYyDkvb/nlKjkuYvliY3liqHlv4XlhYjpmIXor7vmlofmoaPpm7blj43lsITlhajliqjmgIFBbmRyb2lk5o+S5Lu25qGG5p62UHl0aG9u5YWl6Zeo572R57uc54is6Jmr5LmL57K+5Y2O54mI5YiG5biD5byP6YWN572u566h55CG5bmz5Y+wIOS4reaWhyBpT1MgTWFjIOW8gOWPkeWNmuWuouWIl+ihqOWRqOW/l+WNjiDmnLrlmajlrabkuaAg5Y+I56ew6KW/55Oc5Lmm5piv5LiA5pys6L6D5Li65YWo6Z2i55qE5Lmm57GNIOS5puS4reivpue7huS7i+e7jeS6huacuuWZqOWtpuS5oOmihuWfn+S4jeWQjOexu+Wei+eahOeul+azlSDkvovlpoIg55uR552j5a2m5LmgIOaXoOebkeedo+WtpuS5oCDljYrnm5HnnaPlrabkuaAg5by65YyW5a2m5LmgIOmbhuaIkOmZjee7tCDnibnlvoHpgInmi6nnrYkg6K6w5b2V5LqG5pys5Lq65Zyo5a2m5Lmg6L+H56iL5Lit55qE55CG6Kej5oCd6Lev5LiO5omp5bGV55+l6K+G54K5IOW4jOacm+WvueaWsOS6uumYheivu+ilv+eTnOS5puacieaJgOW4ruWKqSDlm73lhoXpppbkuKpTcHJpbmcgQ2xvdWTlvq7mnI3liqHljJZSQkFD55qE566h55CG5bmz5Y+wIOaguOW/g+mHh+eUqOWJjeerr+mHh+eUqGQyIGFkbWlu5Lit5Y+w5qGG5p62IOiusOW+l+S4iui+ueeCueS4qnN0YXIg5YWz5rOo5pu05pawQXBhY2hlIEVDaGFydHMgaW5jdWJhdGluZyDnmoTlvq7kv6HlsI/nqIvluo/niYjmnKxDIOi1hOa6kOWkp+WFqOS4reaWh+eJiCDmoIflh4blupMgV2Vi5bqU55So5qGG5p62IOS6uuW3peaZuuiDvSDmlbDmja7lupMg5Zu+54mH5aSE55CGIOacuuWZqOWtpuS5oCDml6Xlv5cg5Luj56CB5YiG5p6Q562JIOeUsSDlvIDmupDliY3lk6gg5ZKMIENQUOW8gOWPkeiAhSDlvq7kv6Hlhazlj7flm6LpmJ/nu7TmiqTmm7TmlrAgc3RhY2tvdmVyZmxvd+S4ikphdmHnm7jlhbPlm57nrZTmlbTnkIbnv7vor5Eg5Z+65LqOR29vZ2xlIEZsdXR0ZXLnmoRXYW5BbmRyb2lk5a6i5oi356uvIOaUr+aMgUFuZHJvaWTlkoxpT1Mg5YyF5ousQkxvQyBSeERhcnQg5Zu96ZmF5YyWIOS4u+mimOiJsiDlkK/liqjpobUg5byV5a+86aG1IOacrOS7o+eggeW6k+aYr+S9nOiAheWwj+WCheWTpeWkmuW5tOS7juS6i+S4gOe6v+S6kuiBlOe9kSBKYXZhIOW8gOWPkeeahOWtpuS5oOWOhueoi+aKgOacr+axh+aAuyDml6jlnKjkuLrlpKflrrbmj5DkvpvkuIDkuKrmuIXmmbDor6bnu4bnmoTlrabkuaDmlZnnqIsg5L6n6YeN54K55pu05YC+5ZCR57yW5YaZSmF2YeaguOW/g+WGheWuuSDlpoLmnpzmnKzku5PlupPog73kuLrmgqjmj5DkvpvluK7liqkg6K+357uZ5LqI5pSv5oyBIOWFs+azqCDngrnotZ4g5YiG5LqrIEMg6LWE5rqQ5aSn5YWo5Lit5paH54mIIOWMheaLrOS6hiDmnoTlu7rns7vnu58g57yW6K+R5ZmoIOaVsOaNruW6kyDliqDlr4Yg5Yid5Lit6auY55qE5pWZ56iLIOaMh+WNlyDkuabnsY0g5bqT562JIE5FVCBtM3U4IGRvd25sb2FkZXIg5byA5rqQ55qE5ZG95Luk6KGMbTN1OCBITFMgZGFzaOS4i+i9veWZqCDmlK/mjIHmma7pgJpBRVMgMTI4IENCQ+ino+WvhiDlpJrnur/nqIsg6Ieq5a6a5LmJ6K+35rGC5aS0562JIOaUr+aMgeeugOS9k+S4reaWhyDnuYHkvZPkuK3mloflkozoi7HmlocgRW5nbGlzaCBTdXBwb3J0ZWQg5Zu95YaF5L2O5Luj56CB5bmz5Y+w5LuO5Lia6ICF5Lqk5rWBdGNjIHRyYW5zYWN0aW9u5pivVEND5Z6L5LqL5YqhamF2YeWunueOsOiuvuiuoeaooeW8jyBHb2xhbmflrp7njrAg56CU56Oo6K6+6K6h5qih5byPIOivu+S5pueslOiusFZ1ZeaVsOaNruWPr+inhuWMlue7hOS7tuW6kyDnsbvkvLzpmL/ph4xEYXRhViDlpKflsY/mlbDmja7lsZXnpLog5o+Q5L6bU1ZH55qE6L655qGG5Y+K6KOF6aWwIOWbvuihqCDmsLTkvY3lm74g6aOe57q/5Zu+562J57uE5Lu2IOeugOWNleaYk+eUqCDplb/mnJ/mm7TmlrAgUmVhY3TniYjlt7Llj5HluIMg6Ieq5bex5Yqo5omL5YGa6IGK5aSp5py65Zmo5Lq65pWZ56iLIFJlY3ljbGVyVmlld+S+p+a7keiPnOWNlSBJdGVt5ouW5ou9IOa7keWKqOWIoOmZpEl0ZW0g6Ieq5Yqo5Yqg6L295pu05aSaIEhlYWRlclZpZXcgRm9vdGVyVmlldyBJdGVt5YiG57uE6buP6LS0IOiFvuiur+eJqeiBlOe9kee7iOerr+aTjeS9nOezu+e7n+S4gOS4quWwj+W3pyDovbvph4/nmoTmtY/op4jlmajlhoXmoLgg55So5p2l5Y+W5Lujd2tl5ZKMbGliY2Vm5YyF5ZCr576O6aKc562JNCDkvZnnp43lrp7ml7bmu6TplZznm7jmnLog5Y+v5ouN54WnIOW9leWDjyDlm77niYfkv67mlLlzcHJpbmdib290IOahhuaetuS4juWFtuWug+e7hOS7tue7k+WQiOWmguetieeUqOa3seW6puWtpuS5oOWvueWvueiBlCDmioDmnK/pnaLor5Xlv4XlpIfln7rnoYDnn6Xor4YgTGVldGNvZGUg6K6h566X5py65pON5L2c57O757ufIOiuoeeul+acuue9kee7nCDns7vnu5/orr7orqEgSmF2YeWtpuS5oCDpnaLor5XmjIfljZcg5LiA5Lu95ra155uW5aSn6YOo5YiGIEphdmEg56iL5bqP5ZGY5omA6ZyA6KaB5o6M5o+h55qE5qC45b+D55+l6K+GIOWHhuWkhyBKYXZhIOmdouivlSDpppbpgIkgSmF2YUd1aWRlIOeUqOWKqOeUu+eahOW9ouW8j+WRiOeOsOino0xlZXRDb2Rl6aKY55uu55qE5oCd6LevIOS6kuiBlOe9kSBKYXZhIOW3peeoi+W4iOi/m+mYtuefpeivhuWujOWFqOaJq+ebsiDmtrXnm5bpq5jlubblj5Eg5YiG5biD5byPIOmrmOWPr+eUqCDlvq7mnI3liqEg5rW36YeP5pWw5o2u5aSE55CG562J6aKG5Z+f55+l6K+GbWFsbOmhueebruaYr+S4gOWll+eUteWVhuezu+e7nyDljIXmi6zliY3lj7DllYbln47ns7vnu5/lj4rlkI7lj7DnrqHnkIbns7vnu58g5Z+65LqOU3ByaW5nQm9vdCBNeUJhdGlz5a6e546wIOmHh+eUqERvY2tlcuWuueWZqOWMlumDqOe9siDliY3lj7DllYbln47ns7vnu5/ljIXlkKvpppbpobXpl6jmiLcg5ZWG5ZOB5o6o6I2QIOWVhuWTgeaQnOe0oiDllYblk4HlsZXnpLog6LSt54mp6L2mIOiuouWNlea1geeoiyDkvJrlkZjkuK3lv4Mg5a6i5oi35pyN5YqhIOW4ruWKqeS4reW/g+etieaooeWdlyDlkI7lj7DnrqHnkIbns7vnu5/ljIXlkKvllYblk4HnrqHnkIYg6K6i5Y2V566h55CGIOS8muWRmOeuoeeQhiDkv4PplIDnrqHnkIYg6L+Q6JCl566h55CGIOWGheWuueeuoeeQhiDnu5/orqHmiqXooagg6LSi5Yqh566h55CGIOadg+mZkOeuoeeQhiDorr7nva7nrYnmqKHlnZcgR2l0SHVi5Lit5paH5o6S6KGM5qacIOW4ruWKqeS9oOWPkeeOsOmrmOWIhuS8mOengOS4reaWh+mhueebriDmm7Tpq5jmlYjlnLDlkLjmlLblm73kurrnmoTkvJjnp4Dnu4/pqozmiJDmnpwg5qac5Y2V5q+P5ZGo5pu05paw5LiA5qyhIOaVrOivt+WFs+azqCDnrpfms5XpnaLor5Ug566X5rOV55+l6K+GIOmSiOWvueWwj+eZveeahOeul+azleiuree7gyDov5jljIXmi6wgMSDpmL/ph4wg5a2X6IqCIOa7tOa7tCDnmb7nr4flpKfljoLpnaLnu4/msYfmgLsgMiDljYPmnKzlvIDmupDnlLXlrZDkuaYgMyDnmb7lvKDmgJ3nu7Tlr7zlm74g5Y+z5L6n5p2l5LiqIHN0YXIg5ZCnIEVuZ2xpc2ggdmVyc2lvbiBzdXBwb3J0ZWQg6K+K5pat5Yip5ZmoQXJ0aGFz5pWZ56iLIOaKgOacr+agiOekuuS+i+S7o+eggSDlv6vpgJ/nroDljZXkuIrmiYvmlZnnqIsgaHR0cOS4i+i9veW3peWFtyDln7rkuo5odHRw5Luj55CGIOaUr+aMgeWkmui/nuaOpeWIhuWdl+S4i+i9vemYv+mHjOS6keiuoeeul+W5s+WPsOWboumYn+WHuuWTgSDkuLrnm5HmjqfogIznlJ/nmoTmlbDmja7lupPov57mjqXmsaAg5LyB5Lia57qn5L2O5Luj56CB5bmz5Y+wIOWJjeWQjuerr+WIhuemu+aetuaehOW8uuWkp+eahOS7o+eggeeUn+aIkOWZqOiuqeWJjeWQjuerr+S7o+eggeS4gOmUrueUn+aIkCDml6DpnIDlhpnku7vkvZXku6PnoIEg5byV6aKG5paw55qE5byA5Y+R5qih5byPT25saW5lQ29kaW5nIOS7o+eggeeUn+aIkCDmiYvlt6VNRVJHRSDluK7liqlKYXZh6aG555uu6Kej5YazNyAl6YeN5aSN5bel5L2cIOiuqeW8gOWPkeabtOWFs+azqOS4muWKoSDml6Log73lv6vpgJ/mj5Dpq5jmlYjnjocg5biu5Yqp5YWs5Y+46IqC55yB5oiQ5pysIOWQjOaXtuWPiOS4jeWkseeBtea0u+aApyDkuIvmi4nliLfmlrAg5LiK5ouJ5Yqg6L29IOS6jOe6p+WIt+aWsCDmt5jlrp3kuozmpbzmmbrog73kuIvmi4nliLfmlrDmoYbmnrYg5pSv5oyB6LaK55WM5Zue5by5IOi2iueVjOaLluWKqCDlhbfmnInmnoHlvLrnmoTmianlsZXmgKcg6ZuG5oiQ5LqG5Yeg5Y2B56eN54Kr6YW355qESGVhZGVy5ZKMIEZvb3RlciDor6Xpobnnm67lt7LmiJDlip/pm4bmiJAgYWN0dWF0b3Ig55uR5o6nIGFkbWluIOWPr+inhuWMluebkeaOpyBsb2diYWNrIOaXpeW/lyBhb3BMb2cg6YCa6L+HQU9Q6K6w5b2Vd2Vi6K+35rGC5pel5b+XIOe7n+S4gOW8guW4uOWkhOeQhiBqc29u57qn5Yir5ZKM6aG16Z2i57qn5YirIGZyZWVtYXJrZXIg5qih5p2/5byV5pOOIHRoeW1lbGVhZiDmqKHmnb/lvJXmk44gQmVldGwg5qih5p2/5byV5pOOIEVuam95IOaooeadv+W8leaTjiBKZGJjVGVtcGxhdGUg6YCa55SoSkRCQ+aTjeS9nOaVsOaNruW6kyBKUEEg5by65aSn55qET1JN5qGG5p62IG15YmF0aXMg5by65aSn55qET1JN5qGG5p62IOmAmueUqE1hcHBlciDlv6vpgJ/mk43kvZxNeWJhdGlzIFBhZ2VIZWxwZXIg6YCa55So55qETXliYXRpc+WIhumhteaPkuS7tiBteWJhdGlzIHBsdXMg5b+r6YCf5pON5L2cTXliYXRpcyBCZWV0bFNRTCDlvLrlpKfnmoRPUk3moYbmnrYgdSDlvq7kurrkuovmmK/kuIDkuKrliY3lkI7nq6/liIbnprvnmoTkurrlipvotYTmupDnrqHnkIbns7vnu58g6aG555uu6YeH55SoU3ByaW5nQm9vdCBWdWXlvIDlj5Eg56eS5p2A57O757uf6K6+6K6h5LiO5a6e546wIOS6kuiBlOe9keW3peeoi+W4iOi/m+mYtuS4juWIhuaekCBUbyBCZSBUb3AgSmF2YWVyIEphdmHlt6XnqIvluIjmiJDnpZ7kuYvot6/lvqrluo/muJDov5sg5a2m5Lmg5Y2a5a6iU3ByaW5n57O75YiX5rqQ56CBIG1yYmlyZCBjY+W/q+mAnyDnroDljZXpgb/lhY1PT03nmoRqYXZh5aSE55CGRXhjZWzlt6XlhbfpmL/ph4zlt7Tlt7QgTXlTUUwgYmlubG9nIOWinumHj+iuoumYhSDmtojotLnnu4Tku7Yg5LiA5qy+5LyY56eA55qE5byA5rqQ5Y2a5a6i5Y+R5biD5bqU55SoIOWIhuW4g+W8j+S7u+WKoeiwg+W6puW5s+WPsFhYTCBKT0Ig5LiA5qy+6Z2i5ZCR5rOb5YmN56uv5Lqn5ZOB56CU5Y+R5YWo55Sf5ZG95ZGo5pyf55qE5pWI546H5bmz5Y+wIOmdouWQkeS6keWOn+eUn+W+ruacjeWKoeeahOmrmOWPr+eUqOa1geaOp+mYsuaKpOe7hOS7tiDop4bpopHmkq3mlL7lmajmlK/mjIHlvLnluZUg5aSW5oyC5a2X5bmVIOaUr+aMgea7pOmVnCDmsLTljbAgZ2lm5oiq5Zu+IOeJh+WktOW5v+WRiiDkuK3pl7Tlub/lkYog5aSa5Liq5ZCM5pe25pKt5pS+IOaUr+aMgeWfuuacrOeahOaLluWKqCDlo7Dpn7Mg5Lqu5bqm6LCD6IqCIOaUr+aMgei+ueaSrei+uee8k+WtmCDmlK/mjIHop4bpopHoh6rluKZyb3RhdGlvbueahOaXi+i9rCA5IDI3IOS5i+exuyDph43lipvml4vovazkuI7miYvliqjml4vovaznmoTlkIzmraXmlK/mjIEg5pSv5oyB5YiX6KGo5pKt5pS+IOWIl+ihqOWFqOWxj+WKqOeUuyDop4bpopHliqDovb3pgJ/luqYg5YiX6KGo5bCP56qX5Y+j5pSv5oyB5ouW5YqoIOWKqOeUu+aViOaenCDosIPmlbTmr5Tkvosg5aSa5YiG6L6o546H5YiH5o2iIOaUr+aMgeWIh+aNouaSreaUvuWZqCDov5vluqbmnaHlsI/nqpflj6PpooTop4gg5YiX6KGo5YiH5o2i6K+m5oOF6aG16Z2i5peg57yd5pKt5pS+IHJ0c3AgY29uY2F0IG1wZWcg5Y+I5LiA5Liq5bCP5ZWG5Z+OIGxpdGVtYWxsIFNwcmluZyBCb2905ZCO56uvIFZ1ZeeuoeeQhuWRmOWJjeerryDlvq7kv6HlsI/nqIvluo/nlKjmiLfliY3nq68gVnVl55So5oi356e75Yqo56uv5Z+65LqOU3ByaW5nIFNwcmluZ01WQyBNeWJhdGlz5YiG5biD5byP5pWP5o235byA5Y+R57O757uf5p625p6EIOaPkOS+m+aVtOWll+WFrOWFseW+ruacjeWKoeacjeWKoeaooeWdlyDpm4bkuK3mnYPpmZDnrqHnkIYg5Y2V54K555m75b2VIOWGheWuueeuoeeQhiDmlK/ku5jkuK3lv4Mg55So5oi3566h55CGIOaUr+aMgeesrOS4ieaWueeZu+W9lSDlvq7kv6HlubPlj7Ag5a2Y5YKo57O757ufIOmFjee9ruS4reW/gyDml6Xlv5fliIbmnpAg5Lu75Yqh5ZKM6YCa55+l562JIOaUr+aMgeacjeWKoeayu+eQhiDnm5Hmjqflkozov73ouKog5Yqq5Yqb5Li65Lit5bCP5Z6L5LyB5Lia5omT6YCg5YWo5pa55L2NSjJFReS8geS4mue6p+W8gOWPkeino+WGs+aWueahiCDpobnnm67ln7rkuo7nmoTliY3lkI7nq6/liIbnprvnmoTlkI7lj7DnrqHnkIbns7vnu58g6aG555uu6YeH55So5YiG5qih5Z2X5byA5Y+R5pa55byPIOadg+mZkOaOp+WItumHh+eUqCBSQkFDIOaUr+aMgeaVsOaNruWtl+WFuOS4juaVsOaNruadg+mZkOeuoeeQhiDmlK/mjIHkuIDplK7nlJ/miJDliY3lkI7nq6/ku6PnoIEg5pSv5oyB5Yqo5oCB6Lev55SxIOWPsuS4iuacgOeugOWNleeahFNwcmluZyBDbG91ZOaVmeeoi+a6kOeggSBDQVQg5L2c5Li65pyN5Yqh56uv6aG555uu5Z+656GA57uE5Lu2IOaPkOS+m+S6hiBKYXZhIEMgQyBOb2RlIGpzIFB5dGhvbiBHbyDnrYnlpJror63oqIDlrqLmiLfnq68g5bey57uP5Zyo576O5Zui54K56K+E55qE5Z+656GA5p625p6E5Lit6Ze05Lu25qGG5p62IE1WQ+ahhuaetiBSUEPmoYbmnrYg5pWw5o2u5bqT5qGG5p62IOe8k+WtmOahhuaetuetiSDmtojmga/pmJ/liJcg6YWN572u57O757uf562JIOa3seW6pumbhuaIkCDkuLrnvo7lm6Lngrnor4TlkITkuJrliqHnur/mj5Dkvpvns7vnu5/kuLDlr4znmoTmgKfog73mjIfmoIcg5YGl5bq354q25Ya1IOWunuaXtuWRiuitpuetiSBzcHJpbmcgYm9vdCDlrp7ot7XlrabkuaDmoYjkvosg5pivIHNwcmluZyBib290IOWIneWtpuiAheWPiuaguOW/g+aKgOacr+W3qeWbuueahOacgOS9s+Wunui3tSDlj6blpJblhpnljZrlrqIg55SoIE9wZW5Xcml0ZSBTcHJpbmcgQm9vdOWfuuehgOaVmeeoiyBTcHJpbmcgQm9vdCAyIHjniYjmnKzov57ovb3kuK0g5biu5YqpIEFuZHJvaWQgQXBwIOi/m+ihjOe7hOS7tuWMluaUuemAoOeahOi3r+eUseahhuaetiDmj5Dpq5ggQW5kcm9pZCBVSSDlvIDlj5HmlYjnjofnmoQgVUkg5bqT5pe26Ze06YCJ5oup5ZmoIOecgeW4guWMuuS4iee6p+iBlOWKqCBMdWJhbiDpsoHnj63lj6/og73mmK/mnIDmjqXov5Hlvq7kv6HmnIvlj4vlnIjnmoTlm77niYfljovnvKnnrpfms5UgR2l0ZWUg5pyA5pyJ5Lu35YC85byA5rqQ6aG555uuIOWwj+iAjOWFqOiAjOe+jueahOesrOS4ieaWueeZu+W9leW8gOa6kOe7hOS7tiDnm67liY3lt7LmlK/mjIFHaXRodWIgR2l0ZWUg5b6u5Y2aIOmSiemSiSDnmb7luqYgQ29kaW5nIOiFvuiur+S6keW8gOWPkeiAheW5s+WPsCBPU0NoaW5hIOaUr+S7mOWunSBRUSDlvq7kv6Eg5reY5a6dIEdvb2dsZSBGYWNlYm9vayDmipbpn7Mg6aKG6IuxIOWwj+exsyDlvq7ova8g5LuK5pel5aS05p2h5Lq65Lq6IOWNjuS4uiDkvIHkuJrlvq7kv6Eg6YW35a625LmQIEdpdGxhYiDnvo7lm6Ig6aW/5LqG5LmIIOaOqOeJuSDpo57kuaYg5Lqs5LicIOmYv+mHjOS6kSDllpzpqazmi4npm4UgQW1hem9uIFNsYWNr5ZKMIExpbmUg562J56ys5LiJ5pa55bmz5Y+w55qE5o6I5p2D55m75b2VIExvZ2luIHNvIGVhc3kg5LuK5pel5aS05p2h5bGP5bmV6YCC6YWN5pa55qGI57uI5p6B54mIIOS4gOS4quaegeS9juaIkOacrOeahCBBbmRyb2lkIOWxj+W5lemAgumFjeaWueahiCBCYW5uZXIgMiDmnaXkuoYgQW5kcm9pZOW5v+WRiuWbvueJh+i9ruaSreaOp+S7tiDlhoXpg6jln7rkuo5WaWV3UGFnZXIy5a6e546wIEluZGljYXRvcuWSjFVJ6YO95Y+v5Lul6Ieq5a6a5LmJIOmbtuS7o+eggSDng63mm7TmlrAg6Ieq5Yqo5YyWIE9STSDlupMg5ZCO56uv5o6l5Y+j5ZKM5paH5qGj6Zu25Luj56CBIOWJjeerryDlrqLmiLfnq68g5a6a5Yi26L+U5ZueIEpTT04g55qE5pWw5o2u5ZKM57uT5p6E5LiA5Liq5ra155uW5YWt5Liq5LiT5qCP5YiG5biD5byP5raI5oGv6Zif5YiXIOWIhuW4g+W8j+S6i+WKoeeahOS7k+W6kyDluIzmnJvog5blj4vlsI/miYvkuIDmipYg5Y+z5LiK6KeS5p2l5LiqIFN0YXIg5oSf5oGpIDEgMjRNeWJhdGlz6YCa55So5YiG6aG15o+S5Lu2T2tHbyAzIOmch+aSvOadpeiirSDor6XlupPmmK/ln7rkuo4g5Y2P6K6uIOWwgeijheS6hiBPa0h0dHAg55qE572R57uc6K+35rGC5qGG5p62IOavlCBSZXRyb2ZpdCDmm7TnroDljZXmmJPnlKgg5pSv5oyBIFJ4SmF2YSBSeEphdmEyIOaUr+aMgeiHquWumuS5iee8k+WtmCDmlK/mjIHmibnph4/mlq3ngrnkuIvovb3nrqHnkIblkozmibnph4/kuIrkvKDnrqHnkIblip/og73lkKsgRmxpbmsg5YWl6ZeoIOamguW/tSDljp/nkIYg5a6e5oiYIOaAp+iDveiwg+S8mCDmupDnoIHop6PmnpDnrYnlhoXlrrkg5raJ5Y+K562J5YaF5a6555qE5a2m5Lmg5qGI5L6LIOi/mOaciSBGbGluayDokL3lnLDlupTnlKjnmoTlpKflnovpobnnm67moYjkvosgUFZVViDml6Xlv5flrZjlgqgg55m+5Lq/5pWw5o2u5a6e5pe25Y676YeNIOebkeaOp+WRiuitpiDliIbkuqsg5qyi6L+O5aSn5a625pSv5oyB5oiR55qE5LiT5qCPIOWkp+aVsOaNruWunuaXtuiuoeeul+W8leaTjiBGbGluayDlrp7miJjkuI7mgKfog73kvJjljJYg5a6J5Y2T5bmz5Y+w5LiK55qESmF2YVNjcmlwdOiHquWKqOWMluW3peWFtyDvuI/kuIDkuKrmlbTlkIjkuoblpKfph4/kuLvmtYHlvIDmupDpobnnm67pq5jluqblj6/phY3nva7ljJbnmoQgQW5kcm9pZCBNVlAg5b+r6YCf6ZuG5oiQ5qGG5p62IFNwcmluZ+a6kOeggemYheivu+Wkp+aVsOaNruWFpemXqOaMh+WNlyBhbmRyb2lkIDQgNOS7peS4iuayiea1uOW8j+eKtuaAgeagj+WSjOayiea1uOW8j+WvvOiIquagj+euoeeQhiDpgILphY3mqKrnq5blsY/liIfmjaIg5YiY5rW35bGPIOi9r+mUruebmOW8ueWHuuetiemXrumimCDlj6/ku6Xkv67mlLnnirbmgIHmoI/lrZfkvZPpopzoibLlkozlr7zoiKrmoI/lm77moIfpopzoibIg5Lul5Y+K5LiN5Y+v5L+u5pS55a2X5L2T6aKc6Imy5omL5py655qE6YCC6YWNIOmAgueUqOS6juS4gOWPpeS7o+eggei9u+advuWunueOsCDku6Xlj4rlr7liYXLnmoTlhbbku5borr7nva4g6K+m6KeBUkVBRE1FIOeugOS5puivt+WPguiAgyB3d3cgamlhbnNodSBjb20gcCAyYTg4NGUyMTFhNjLkuJrlhoXkuLrmlbDkuI3lpJroh7Tlipvkuo7mnoHoh7TkvZPpqoznmoTotoXlvLrlhajoh6rnoJTot6jlubPlj7Agd2luZG93cyBhbmRyb2lkIGlPUyDmtYHlqpLkvZPlhoXmoLgg6YCa6L+H5qih5Z2X5YyW6Ieq55Sx57uE5ZCIIOaUr+aMgeWunuaXtlJUTVDmjqjmtYEgUlRTUOaOqOa1gSBSVE1Q5pKt5pS+5ZmoIFJUU1Dmkq3mlL7lmagg5b2V5YOPIOWkmui3r+a1geWqkuS9k+i9rOWPkSDpn7Pop4bpopHlr7zmkq0g5Yqo5oCB6KeG6aKR5ZCI5oiQIOmfs+mikea3t+mfsyDnm7Tmkq3kupLliqgg5YaF572u6L276YeP57qnUlRTUOacjeWKoeetiSDmr5Tlv6vmm7Tlv6sg5Lia55WM55yf5q2j6Z2g6LCx55qE6LaF5L2O5bu26L+f55u05pKtU0RLIDHnp5LlhoUg5L2O5bu26L+f5qih5byP5LiLMiA0IG1zIERhdGFY5piv6Zi/6YeM5LqRRGF0YVdvcmtz5pWw5o2u6ZuG5oiQ55qE5byA5rqQ54mI5pysIG1hbGzlrabkuaDmlZnnqIsg5p625p6EIOS4muWKoSDmioDmnK/opoHngrnlhajmlrnkvY3op6PmnpAgbWFsbOmhueebriA0IGsgc3RhciDmmK/kuIDlpZfnlLXllYbns7vnu58g5L2/55So546w6Zi25q615Li75rWB5oqA5pyv5a6e546wIOa2teebluS6huetieaKgOacryDph4fnlKhEb2NrZXLlrrnlmajljJbpg6jnvbIgQW5kcm9pZOW8gOa6kOW8ueW5leW8leaTjiDng4jnhLDlvLnluZXkvb8g772ec3ByaW5nIGNsb3VkIHZ1ZSBvQXV0aDIg5YWo5a625qG25a6e5oiYIOWJjeWQjuerr+WIhuemu+aooeaLn+WVhuWfjiDlrozmlbTnmoTotK3nianmtYHnqIsg5ZCO56uv6L+Q6JCl5bmz5Y+wIOWPr+S7peWunueOsOW/q+mAn+aQreW7uuS8geS4mue6p+W+ruacjeWKoemhueebriDmlK/mjIHlvq7kv6HnmbvlvZXnrYnkuInmlrnnmbvlvZUg5LiA5Liq5Z+65LqOU3ByaW5nIEJvb3QgTXlCYXRpc+eahOenjeWtkOmhueebriDnlKjkuo7lv6vpgJ/mnoTlu7rkuK3lsI/lnotBUEkgUkVTVGZ1bCBBUEnpobnnm64g5by65aSnIOWPr+WumuWItiDmmJPmianlsZXnmoQgVmlld1BhZ2VyIOaMh+ekuuWZqOahhuaetiDmmK/nmoTmnIDkvbPmm7/ku6Plk4Eg5pSv5oyB6KeS5qCHIOabtOaUr+aMgeWcqOmdnlZpZXdQYWdlcuWcuuaZr+S4i+S9v+eUqCDkvb/nlKhoaWRlIHNob3cg5YiH5o2iRnJhZ21lbnTmiJbkvb/nlKhzZSAyN+WkqeaIkOS4ukphdmHlpKfnpZ7lronljZPlrabkuaDnrJTorrAgY2ltIGNyb3NzIElNIOmAgueUqOS6juW8gOWPkeiAheeahOWIhuW4g+W8j+WNs+aXtumAmuiur+ezu+e7n0FuZHJvaWTkuIrkuIDkuKrkvJjpm4Ug5LiH6IO96Ieq5a6a5LmJVUkg5Lu/aU9TIOaUr+aMgeWeguebtCDmsLTlubPmlrnlkJHliIfmjaIg5pSv5oyB5ZGo6KeG5Zu+IOiHquWumuS5ieWRqOi1t+WniyDmgKfog73pq5jmlYjnmoTml6Xljobmjqfku7Yg5pSv5oyB54Ot5o+S5ouU5a6e546w55qEVUnlrprliLYg5pSv5oyB5qCH6K6wIOiHquWumuS5ieminOiJsiDlhpzljoYg6Ieq5a6a5LmJ5pyI6KeG5Zu+5ZCE56eN5pi+56S65qih5byP562JIENhbnZhc+e7mOWItiDpgJ/luqblv6sg5Y2g55So5YaF5a2Y5L2OIOS9oOecn+eahOaDs+S4jeWIsOaXpeWOhuWxheeEtui/mOWPr+S7peWmguatpOS8mOmbhWhzd2ViIGhhyopzIHfJm2Ig5piv5LiA5Liq5Z+65LqOc3ByaW5nIGJvb3QgMiB45byA5Y+RIOmmluS4quS9v+eUqOWFqOWTjeW6lOW8j+e8lueoi+eahOS8geS4mue6p+WQjuWPsOeuoeeQhuezu+e7n+WfuuehgOmhueebriBuZXdiZWUgbWFsbCDpobnnm64g5paw6JyC5ZWG5Z+OIOaYr+S4gOWll+eUteWVhuezu+e7nyDljIXmi6wgbmV3YmVlIG1hbGwg5ZWG5Z+O57O757uf5Y+KIG5ld2JlZSBtYWxsIGFkbWluIOWVhuWfjuWQjuWPsOeuoeeQhuezu+e7nyDln7rkuo4gU3ByaW5nIEJvb3QgMiBYIOWPiuebuOWFs+aKgOacr+agiOW8gOWPkSDliY3lj7DllYbln47ns7vnu5/ljIXlkKvpppbpobXpl6jmiLcg5ZWG5ZOB5YiG57G7IOaWsOWTgeS4iue6vyDpppbpobXova7mkq0g5ZWG5ZOB5o6o6I2QIOWVhuWTgeaQnOe0oiDllYblk4HlsZXnpLog6LSt54mp6L2mIOiuouWNlee7k+eulyDorqLljZXmtYHnqIsg5Liq5Lq66K6i5Y2V566h55CGIOS8muWRmOS4reW/gyDluK7liqnkuK3lv4PnrYnmqKHlnZcg5ZCO5Y+w566h55CG57O757uf5YyF5ZCr5pWw5o2u6Z2i5p2/IOi9ruaSreWbvueuoeeQhiDllYblk4HnrqHnkIYg6K6i5Y2V566h55CGIOS8muWRmOeuoeeQhiDliIbnsbvnrqHnkIYg6K6+572u562J5qih5Z2XIG1hbGwgc3dhcm3mmK/kuIDlpZflvq7mnI3liqHllYbln47ns7vnu58g6YeH55So5LqG562J5qC45b+D5oqA5pyvIOWQjOaXtuaPkOS+m+S6huWfuuS6jlZ1ZeeahOeuoeeQhuWQjuWPsOaWueS+v+W/q+mAn+aQreW7uuezu+e7nyBtYWxsIHN3YXJt5Zyo55S15ZWG5Lia5Yqh55qE5Z+656GA6ZuG5oiQ5LqG5rOo5YaM5Lit5b+DIOmFjee9ruS4reW/gyDnm5HmjqfkuK3lv4Mg572R5YWz562J57O757uf5Yqf6IO9IOaWh+aho+m9kOWFqCDpmYTluKblhajlpZdTcHJpbmcgQ2xvdWTmlZnnqIsg6ZiF6K+75piv5LiA5qy+5Y+v5Lul6Ieq5a6a5LmJ5p2l5rqQ6ZiF6K+7572R57uc5YaF5a6555qE5bel5YW3IOS4uuW5v+Wkp+e9kee7nOaWh+WtpueIseWlveiAheaPkOS+m+S4gOenjeaWueS+vyDlv6vmjbfoiJLpgILnmoTor5Xor7vkvZPpqowgU3ByaW5nIENsb3Vk5Z+656GA5pWZ56iLIOaMgee7rei/nui9veabtOaWsOS4remYv+mHjOW3tOW3tOWIhuW4g+W8j+aVsOaNruW6k+WQjOatpeezu+e7nyDop6PlhrPkuK3nvo7lvILlnLDmnLrmiL8g5Z+65LqO6LC35q2M5pyA5pawQUFD5p625p6EIE1WVk3orr7orqHmqKHlvI/nmoTkuIDlpZflv6vpgJ/lvIDlj5HlupMg5pW05ZCIT2tSeEphdmEgUmV0cm9maXQgR2xpZGXnrYnkuLvmtYHmqKHlnZcg5ruh6Laz5pel5bi45byA5Y+R6ZyA5rGCIOS9v+eUqOivpeahhuaetuWPr+S7peW/q+mAn+W8gOWPkeS4gOS4qumrmOi0qOmHjyDmmJPnu7TmiqTnmoRBbmRyb2lk5bqU55SoIOWfuuS6jlNwcmluZ0Nsb3VkMiAx55qE5b6u5pyN5Yqh5byA5Y+R6ISa5omL5p62IOaVtOWQiOS6huetiSDmnI3liqHmsrvnkIbmlrnpnaLlvJXlhaXnrYkg6K6p6aG555uu5byA5Y+R5b+r6YCf6L+b5YWl5Lia5Yqh5byA5Y+RIOiAjOS4jemcgOi/h+WkmuaXtumXtOiKsei0ueWcqOaetuaehOaQreW7uuS4iiDmjIHnu63mm7TmlrDkuK3ln7rkuo5TT0HmnrbmnoTnmoTliIbluIPlvI/nlLXllYbotK3nianllYbln44g5YmN5ZCO56uv5YiG56a7IOWJjeWPsOWVhuWfjiDlhajlrrbmobYg5ZCO5Y+w566h55CG57O757uf562J5pivIOmavuW+l+S4gOingSDnmoQgSmV0cGFjayBNVlZNIOacgOS9s+Wunui3tSDlnKgg5Lul566A6amt57mBIOeahOS7o+eggeS4rSDlr7kg6KeG5Zu+5o6n5Yi25ZmoIOS5g+iHsyDmoIflh4bljJblvIDlj5HmqKHlvI8g5b2i5oiQ5q2j56GuIOa3seWFpeeahOeQhuinoyBW6YOo6JC9IFZ1ZSBTcHJpbmdCb2905a6e546w55qE5aSa55So5oi35Y2a5a6i566h55CG5bmz5Y+wIEFuZHJvaWQgU2lnbmF0dXJlIFYyIFNjaGVtZeetvuWQjeS4i+eahOaWsOS4gOS7o+a4oOmBk+WMheaJk+WMheelnuWZqOWNs+aXtumAmuiuryBJTSDns7vnu5/lpJrnp43nvJbnqIvor63oqIDlrp7njrAgTGVldENvZGUg5YmR5oyHIE9mZmVyIOesrCAyIOeJiCDnqIvluo/lkZjpnaLor5Xph5Hlhbgg56ysIDYg54mIIOmimOino+S4k+mXqOS4uuWImuW8gOWni+WIt+mimOeahOWQjOWtpuWHhuWkh+eahOeul+azleWfuuWcsCDmsqHmnInmnIDnu4blj6rmnInmm7Tnu4Yg56uL5b+X55So5Yqo55S75bCG5pmm5rap6Zq+5oeC55qE566X5rOV6K+055qE6YCa5L+X5piT5oeCIGFuc2rliIbor40gaWN055qE55yf5q2jamF2YeWunueOsCDliIbor43mlYjmnpzpgJ/luqbpg73otoXov4flvIDmupDniYjnmoRpY3Qg5Lit5paH5YiG6K+NIOS6uuWQjeivhuWIqyDor43mgKfmoIfms6gg55So5oi36Ieq5a6a5LmJ6K+N5YW4IGJvb2sg5Lu76ZiFIOe9kee7nOWwj+ivtOmYheivu+WZqCAzROe/u+mhteaViOaenCB0eHQgcGRmIGVwdWLkuabnsY3pmIXor7sgV2lmaeS8oOS5piBMZWV0Q29kZeWIt+mimOiusOW9leS4jumdouivleaVtOeQhm15YmF0aXMgZ2VuZXJhdG9y55WM6Z2i5bel5YW3IOiuqeS9oOeUn+aIkOS7o+eggeabtOeugOWNleabtOW/q+aNt1NwcmluZyBDbG91ZCDlrabkuaDmoYjkvosg5pyN5Yqh5Y+R546wIOacjeWKoeayu+eQhiDpk77ot6/ov73ouKog5pyN5Yqh55uR5o6n562JIFhQb3B1cDIg54mI5pys6YeN56OF5p2l6KKtIDLlgI3ku6XkuIrmgKfog73mj5DljYcg5bim5p2l5Y+v6KeC55qE5Yqo55S75oCn6IO95LyY5YyW5ZKM5Lqk5LqS57uG6IqC55qE5o+Q5Y2HIOWKn+iDveW8uuWkpyDkuqTkupLkvJjpm4Ug5Yqo55S75Lid5ruR55qE6YCa55So5by556qXIOWPr+S7peabv+S7o+etiee7hOS7tiDoh6rluKbljYHlh6Dnp43mlYjmnpzoia/lpb3nmoTliqjnlLsg5pSv5oyB5a6M5YWo55qEVUnlkozliqjnlLvoh6rlrprkuYnmkJzni5Dop4bpopEgc29odSB0diBSZWRpc+engeacieS6keW5s+WPsHNwcmluZyBib2905omT6YCg5paH5Lu25paH5qGj5Zyo57q/6aKE6KeI6aG555uu5p2D6ZmQ566h55CG57O757ufIOmihOiniOWcsOWdgCA0NyAxIDQgNyAxMzggbG9naW7pm7blj43lsITlhajliqjmgIFBbmRyb2lk5o+S5Lu25qGG5p625YiG5biD5byP6YWN572u566h55CG5bmz5Y+wIOmAmueUqCBJTSDogYrlpKkgVUkg57uE5Lu2IOW3sue7j+WQjOaXtuaUr+aMgSBBbmRyb2lkIGlPUyBSTiDmiYvmiormiYvmlZnkvaDmlbTlkIjmnIDkvJjpm4VTU03moYbmnrYgU3ByaW5nTVZDIFNwcmluZyBNeUJhdGlz5o2i6IKk5qGG5p62IOaegeS9jueahOWtpuS5oOaIkOacrCDmnoHlpb3nmoTnlKjmiLfkvZPpqowg5LiA6KGMIOS7o+eggeWwseWPr+S7peWunueOsOaNouiCpCDkvaDlgLzlvpfmi6XmnIkgSlZNIOW6leWxguWOn+eQhuacgOWFqOefpeivhuaAu+e7kyDlm73lhoXpppbkuKpTcHJpbmcgQ2xvdWTlvq7mnI3liqHljJZSQkFD55qE566h55CG5bmz5Y+wIOaguOW/g+mHh+eUqOWJjeerr+mHh+eUqGQyIGFkbWlu5Lit5Y+w5qGG5p62IOiusOW+l+S4iui+ueeCueS4qnN0YXIg5YWz5rOo5pu05pawdGNjIHRyYW5zYWN0aW9u5pivVEND5Z6L5LqL5YqhamF2YeWunueOsCBSZWN5Y2xlclZpZXfkvqfmu5Hoj5zljZUgSXRlbeaLluaLvSDmu5HliqjliKDpmaRJdGVtIOiHquWKqOWKoOi9veabtOWkmiBIZWFkZXJWaWV3IEZvb3RlclZpZXcgSXRlbeWIhue7hOm7j+i0tCDljIXlkKvnvo7popznrYk0IOS9meenjeWunuaXtua7pOmVnOebuOacuiDlj6/mi43nhacg5b2V5YOPIOWbvueJh+S/ruaUuXNwcmluZ2Jvb3Qg5qGG5p625LiO5YW25a6D57uE5Lu257uT5ZCI5aaC562J5a6J5Y2T6YCJ5oup5Zmo57G75bqTIOWMheaLrOaXpeacn+WPiuaXtumXtOmAieaLqeWZqCDlj6/nlKjkuo7lh7rnlJ/ml6XmnJ8g6JCl5Lia5pe26Ze0562JIOWNlemhuemAieaLqeWZqCDlj6/nlKjkuo7mgKfliKsg5rCR5pePIOiBjOS4miDlrabljoYg5pif5bqn562JIOS6jOS4iee6p+iBlOWKqOmAieaLqeWZqCDlj6/nlKjkuo7ovabniYzlj7cg5Z+66YeR5a6a5oqV5pel5pyf562JIOWfjuW4guWcsOWdgOmAieaLqeWZqCDliIbnnIHnuqcg5Zyw5biC57qn5Y+K5Yy65Y6/57qnIOaVsOWtl+mAieaLqeWZqCDlj6/nlKjkuo7lubTpvoQg6Lqr6auYIOS9k+mHjSDmuKnluqbnrYkg5pel5Y6G6YCJ5pel5pyf5oup5ZmoIOWPr+eUqOS6jumFkuW6l+WPiuacuuelqOmihOWumuaXpeacnyDpopzoibLpgInmi6nlmagg5paH5Lu25Y+K55uu5b2V6YCJ5oup5Zmo562JIEphdmHlt6XnqIvluIjpnaLor5XlpI3kuaDmjIfljZcg5pys5LuT5bqT5ra155uW5aSn6YOo5YiGSmF2Yeeoi+W6j+WRmOaJgOmcgOimgeaOjOaPoeeahOaguOW/g+efpeivhiDmlbTlkIjkuobkupLogZTnvZHkuIrnmoTlvojlpJrkvJjotKhKYXZh5oqA5pyv5paH56ugIOWKm+axguaJk+mAoOS4uuacgOWujOaVtOacgOWunueUqOeahEphdmHlvIDlj5HogIXlrabkuaDmjIfljZcg5aaC5p6c5a+55L2g5pyJ5biu5YqpIOe7meS4qnN0YXLlkYror4nmiJHlkKcg6LCi6LCiIEFuZHJvaWQgTVZQIOW/q+mAn+W8gOWPkeahhuaetiDlgZrlm73lhoUg56S65L6L5pyA5YWo6Z2iIOazqOmHiuacgOivpue7hiDkvb/nlKjmnIDnroDljZUg5Luj56CB5pyA5Lil6LCoIOeahCBBbmRyb2lkIOW8gOa6kCBVSSDmoYbmnrblh6DooYzku6PnoIHlv6vpgJ/pm4bmiJDkuoznu7TnoIHmiavmj4/lip/og71NZXRlclNwaGVyZSDmmK/kuIDnq5nlvI/lvIDmupDmjIHnu63mtYvor5XlubPlj7Ag5ra155uW5rWL6K+V6Lef6LiqIOaOpeWPo+a1i+ivlSDmgKfog73mtYvor5Ug5Zui6Zif5Y2P5L2c562J5Yqf6IO9IOWFqOmdouWFvOWuuSBKTWV0ZXIgUG9zdG1hbiBTd2FnZ2VyIOetieW8gOa6kCDkuLvmtYHmoIflh4Yg6K6w5b2V5ZCE56eN5a2m5Lmg56yU6K6wIOeul+azlSBKYXZhIOaVsOaNruW6kyDlubblj5Eg5LiL5LiA5LujQW5kcm9pZOaJk+WMheW3peWFtyAxIOS4qua4oOmBk+WMheWPqumcgOimgTEg56eS6ZKf6IqL6YGTIG1hbGwg5ZWG5Z+OIOWfuuS6juW+ruacjeWKoeeahOaAneaDsyDmnoTlu7rlnKggQjJDIOeUteWVhuWcuuaZr+S4i+eahOmhueebruWunuaImCDmoLjlv4PmioDmnK/moIgg5pivIFNwcmluZyBCb290IER1YmJvIOacquadpSDkvJrph43mnoTmiJAgU3ByaW5nIENsb3VkIEFsaWJhYmEgQW5kcm9pZCDkuIfog73nmoTnrYkg5pSv5oyB5aSa56eNSXRlbeexu+Wei+eahOaDheWGtSBsYW5wcm94eeaYr+S4gOS4quWwhuWxgOWfn+e9keS4quS6uueUteiEkSDmnI3liqHlmajku6PnkIbliLDlhaznvZHnmoTlhoXnvZHnqb/pgI/lt6Xlhbcg5pSv5oyBdGNw5rWB6YeP6L2s5Y+RIOWPr+aUr+aMgeS7u+S9lXRjcOS4iuWxguWNj+iuriDorr/pl67lhoXnvZHnvZHnq5kg5pys5Zyw5pSv5LuY5o6l5Y+j6LCD6K+VIHNzaOiuv+mXriDov5znqIvmoYzpnaIg55uu5YmN5biC6Z2i5LiK5o+Q5L6b57G75Ly85pyN5Yqh55qE5pyJ6Iqx55Sf5aOzIFRlYW1WaWV3IEdvVG9NeUNsb3Vk562J562JIOS9huimgeS9v+eUqOesrOS4ieaWueeahOWFrOe9keacjeWKoeWZqOWwseW/hemhu+S4uuesrOS4ieaWueS7mOi0uSDlubbkuJTov5nkupvmnI3liqHpg73mnInlkITnp43lkITmoLfnmoTpmZDliLYg5q2k5aSWIOeUseS6juaVsOaNruWMheS8mua1gee7j+esrOS4ieaWuSDlm6DmraTlr7nmlbDmja7lronlhajkuZ/mmK/kuIDlpKfpmpDmgqMg5oqA5pyv5Lqk5rWBUVHnvqQgMSA2NzQyNDMzIOabtOS8mOmbheeahOmpvui9puS9k+mqjOS4i+i9veWPr+S7peW+iOeugOWNlSDvuI8g5LqR6ZiFIOS4gOasvuWfuuS6jue9keaYk+S6kemfs+S5kFVJIOS9v+eUqOeOqeaetuaehOW8gOWPkeeahOespuWQiEdvb2dsZSBNYXRlcmlhbCBEZXNpZ27nmoRBbmRyb2lk5a6i5oi356uv5byA5rqQ55qEIE1hdGVyaWFsIERlc2lnbiDosYbnk6PlrqLmiLfnq6/kuIDmrL7pkojlr7nns7vnu59Qb3B1cFdpbmRvd+S8mOWMlueahFBvcHVw5bqTIOWKn+iDveW8uuWkpyDmlK/mjIHog4zmma/mqKHns4og5L2/55So566A5Y2VIOS9oOS8mueIseS4iuS7lueahCBQTERyb2lkUGxheWVyIOaYr+S4g+eJm+aOqOWHuueahOS4gOasvuWFjei0ueeahOmAgueUqOS6jiBBbmRyb2lkIOW5s+WPsOeahOaSreaUvuWZqCBTREsg6YeH55So5YWo6Ieq56CU55qE6Leo5bmz5Y+w5pKt5pS+5YaF5qC4IOaLpeacieS4sOWvjOeahOWKn+iDveWSjOS8mOW8gueahOaAp+iDvSDlj6/pq5jluqblrprliLbljJblkozkuozmrKHlvIDlj5Eg6K+l6aG555uu5bey5YGc5q2i57u05oqkIDkgUG9ybiBBbmRyb2lkIOWuouaIt+erryDnqoHnoLTmuLjlrqLmr4/lpKnop4LnnIsxIOasoeinhumikeeahOmZkOWItiDov5jlj6/ku6XkuIvovb3op4bpopEg77iP6JOd57u/IOeBsOW6piDot6/nlLEg6ZmQ5rWBIOeGlOaWrSDpmY3nuqcg6ZqU56a7IOi/vei4qiDmtYHph4/mn5PoibIg5pWF6Zqc6L2s56e75LiA5pys5YWz5LqO5o6S5bqP566X5rOV55qEIEdpdEJvb2sg5Zyo57q/5Lmm57GNIOWNgeWkp+e7j+WFuOaOkuW6j+eul+azlSDlpJror63oqIDlrp7njrAg5aSa56eN5LiL5ouJ5Yi35paw5pWI5p6cIOS4iuaLieWKoOi9veabtOWkmiDlj6/phY3nva7oh6rlrprkuYnlpLTpg6jlub/lkYrkvY3lrozlhajku7/lvq7kv6HnmoTlm77niYfpgInmi6kg5bm25LiU5o+Q5L6b5LqG5aSa56eN5Zu+54mH5Yqg6L295o6l5Y+jIOmAieaLqeWbvueJh+WQjuWPr+S7peaXi+i9rCDlj6/ku6Xoo4HliarmiJDnn6nlvaLmiJblnIblvaIg5Y+v5Lul6YWN572u5ZCE56eN5YW25LuW55qE5Y+C5pWwU29sb1BpIOiHquWKqOWMlua1i+ivleW3peWFt+m+meaenOaUr+S7mOezu+e7nyByb25jb28gcGF5IOaYr+WbveWGhemmluasvuW8gOa6kOeahOS6kuiBlOe9keaUr+S7mOezu+e7nyDmi6XmnInni6znq4vnmoTotKbmiLfkvZPns7sg55So5oi35L2T57O7IOaUr+S7mOaOpeWFpeS9k+ezuyDmlK/ku5jkuqTmmJPkvZPns7sg5a+56LSm5riF57uT566X5L2T57O7IOebruagh+aYr+aJk+mAoOS4gOasvumbhuaIkOS4u+a1geaUr+S7mOaWueW8j+S4lOi9u+mHj+aYk+eUqOeahOaUr+S7mOaUtuasvuezu+e7nyDmu6HotrPkupLogZTnvZHkuJrliqHns7vnu5/miZPpgJrmlK/ku5jpgJrpgZPlrp7njrDmlK/ku5jmlLbmrL7lkozkuJrliqHotYTph5HnrqHnkIbnrYnlip/og70g6ZSu55uY6Z2i5p2/5Yay56qBIOW4g+WxgOmXquWKqOWkhOeQhuaWueahiCDlkpXms6HlrabpmaLlrp7miJjpobnnm64g5Z+65LqOU3ByaW5nQm9vdCBEdWJib+aehOW7uueahOeUteWVhuW5s+WPsCDlvq7mnI3liqHmnrbmnoQg5ZWG5Z+OIOeUteWVhiDlvq7mnI3liqEg6auY5bm25Y+RIGthZmthIEVsYXN0aWNzZWFyY2jlgZzovablnLrns7vnu5/mupDnoIEg5YGc6L2m5Zy65bCP56iL5bqPIOaZuuiDveWBnOi9piBQYXJraW5nIHN5c3RlbSDlip/og73ku4vnu40g4pGg5YW85a655biC6Z2i5LiK5Li75rWB55qE5aSa5a6255u45py6IOeQhuiuuuS4iuWFvOWuueaJgOacieehrOS7tiDlj6/ngbXmtLvmianlsZUg4pGh55u45py66K+G5Yir5ZCO5pWw5o2u6Ieq5Yqo5LiK5Lyg5Yiw5LqR56uv5bm26K6w5b2VIOagoemqjOebuOacuuWUr+S4gGlk5ZKM56Gs5Lu25bqP5YiX5Y+3IOmYsuatoumdnuazleaVsOaNruW9leWFpSDikaLnlKjmiLfmiYvmnLrmn6Xor6LlgZzovaborrDlvZXor6bmg4Xlj6/oh6rkuLvnvLTotLkg5pSv5oyB5b6u5L+hIOaUr+S7mOWunSDpk7booYzmjqXlj6PmlK/ku5gg5pSv5oyB5q+P5Liq5YGc6L2m5Zy65oyH5a6a5LiN5ZCM55qE5ZWG5oi36L+b6KGM5pS25qy+IOaUr+S7mOWQjuWHuuWcuuWcqOWFjei0ueaXtumXtOWGheS8muiHquWKqOaKrOadhiDikaPmlK/mjIFhcHDkuIrmn6Xor6LpmYTov5HlgZzovablnLog5a+86IiqIOWPr+eUqOi9puS9jeaVsCDlgZzovablnLrotLnnlKgg5LyY5oOg5Yi4IOivhOWIhiDor4TorrrnrYkg5Y+v6aKE57qm6L2m5L2NIOKRpOaWreeUteaWree9keaUr+aMgeWyl+S6reS6uuWRmOS9v+eUqGFwcOWPr+aOpeeuoeehrOS7tui/m+ihjOWBnOi9puiusOW9leeahOW9leWFpSDmioDmnK/mnrbmnoQg5ZCO56uv5byA5Y+R6K+t6KiAamF2YSDmoYbmnrZvYXV0aDIgc3ByaW5nIOaIkOmVv+i3r+e6vyDkvYblrabliLDkuI3ku4Xku4XmmK9KYXZhIOS4mueVjOmmluS4quaUr+aMgea4kOi/m+W8j+e7hOS7tuWMluaUuemAoOeahEFuZHJvaWTnu4Tku7bljJblvIDmupDmoYbmnrYg5pSv5oyB6Leo6L+b56iL6LCD55SoU3ByaW5nQm9vdDIg5LuO5YWl6Zeo5Yiw5a6e5oiYIOaXqOWcqOaJk+mAoOWcqOe6v+acgOS9s+eahCBKYXZhIOWtpuS5oOeslOiusCDlkKvljZrlrqLorrLop6PlkozmupDnoIHlrp7kvosg5YyF5ousIEphdmEgU0Ug5ZKMIEphdmEgV2ViSmF2YeiviuaWreW3peWFt+W5tOiWqueZvuS4h+S6kuiBlOe9keaetuaehOW4iOivvueoi+aWh+aho+WPiua6kOeggSDlhazlvIDpg6jliIYgQW5kcm9pZEh0dHBDYXB0dXJl572R57uc6K+K5pat5bel5YW3IOaYr+S4gOasvkFuZHJvaWTmiYvmnLrmipPljIXova/ku7Yg5Li76KaB5Yqf6IO95YyF5ousIOaJi+acuuerr+aKk+WMhSBQSU5HIEROUyBUcmFjZVJvdXRl6K+K5patIOaKk+WMhUhBUuaVsOaNruS4iuS8oOWIhuS6qyDkvaDkuZ/lj6/ku6XnnIvmiJDmmK9BbmRyb2lk54mI55qEIEZpZGRsZXIgbyDov5nlj6/og73mmK/lj7LkuIrlip/og73mnIDlhajnmoRKYXZh5p2D6ZmQ6K6k6K+B5qGG5p62IOebruWJjeW3sumbhuaIkCDnmbvlvZXorqTor4Eg5p2D6ZmQ6K6k6K+BIOWIhuW4g+W8j1Nlc3Npb27kvJror50g5b6u5pyN5Yqh572R5YWz6Ym05p2DIOWNleeCueeZu+W9lSBPQXV0aDIg6Lii5Lq65LiL57q/IFJlZGlz6ZuG5oiQIOWJjeWQjuWPsOWIhuemuyDorrDkvY/miJHmqKHlvI8g5qih5ouf5LuW5Lq66LSm5Y+3IOS4tOaXtui6q+S7veWIh+aNoiDotKblj7flsIHnpoEg5aSa6LSm5Y+36K6k6K+B5L2T57O7IOazqOino+W8j+mJtOadgyDot6/nlLHmi6bmiKrlvI/pibTmnYMg6Iqx5byPdG9rZW7nlJ/miJAg6Ieq5Yqo57ut562+IOWQjOerr+S6kuaWpeeZu+W9lSDkvJror53msrvnkIYg5a+G56CB5Yqg5a+GIGp3dOmbhuaIkCBTcHJpbmfpm4bmiJAgV2ViRmx1eOmbhuaIkCBBbmRyb2lk5bmz5Y+w5LiL55qE5a+M5paH5pys6Kej5p6Q5ZmoIOaUr+aMgUh0bWzlkoxNYXJrZG93buaZuuiDveWbvueJh+ijgeWJquahhuaetiDoh6rliqjor4bliKvovrnmoYYg5omL5Yqo6LCD6IqC6YCJ5Yy6IOS9v+eUqOmAj+inhuWPmOaNouijgeWJquW5tuefq+ato+mAieWMuiDpgILnlKjkuo7ouqvku73or4Eg5ZCN54mHIOaWh+aho+etieeFp+eJh+eahOijgeWJqiDkv5flkI0g5Y+v5Z6C55u06LeRIOWPr+awtOW5s+i3keeahOi3kemprOeBryDlrablkI0g5Y+v5Z6C55u057+7IOWPr+awtOW5s+e/u+eahOe/u+mhteWFrOWRiiDlsI/pqazlk6XmioDmnK/lkajmiqUgQW5kcm9pZCBWaWRlbyBQbGF5ZXIg5a6J5Y2T6KeG6aKR5pKt5pS+5ZmoIOWwgeijheaooeS7v+aKlumfs+W5tuWunueOsOmihOWKoOi9vSDliJfooajmkq3mlL4g5oKs5rWu5pKt5pS+IOW5v+WRiuaSreaUviDlvLnluZUg6YeN5a2mSmF2YeiuvuiuoeaooeW8jyDmmK/kuIDmnKzkupLogZTnvZHnnJ/lrp7moYjkvovlrp7ot7XkuabnsY0g5Lul6JC95Zyw6Kej5Yaz5pa55qGI5Li65qC45b+DIOS7juWunumZheS4muWKoeS4reaKveemu+WHuiDkuqTmmJMg6JCl6ZSAIOenkuadgCDkuK3pl7Tku7Yg5rqQ56CB562JMjLkuKrnnJ/lrp7lnLrmma8g5p2l5a2m5Lmg6K6+6K6h5qih5byP55qE6L+Q55SoIOasoui/juWFs+azqOWwj+WCheWTpSDlvq7kv6EgZnVzdGFjayDlhazkvJflj7cgYnVnc3RhY2vomavmtJ7moIgg5Y2a5a6iIGJ1Z3N0YWNrIGNubXliYXRpc+a6kOeggeS4reaWh+azqOmHiuS4gOasvuW8gOa6kOeahEdJRuWcqOe6v+WIhuS6q0FwcCDkuZDotqPlsLHopoHlkozkuJbnlYzliIbkuqsgTVB1c2jlvIDmupDlrp7ml7bmtojmga/mjqjpgIHns7vnu5/lnKjnur/kupHnm5gg572R55uYIE9uZURyaXZlIOS6keWtmOWCqCDnp4HmnInkupEg5a+56LGh5a2Y5YKoIGg1YWnln7rkuo5TcHJpbmcgQm9vdCAyIHjnmoTkuIDnq5nlvI/liY3lkI7nq6/liIbnprvlv6vpgJ/lvIDlj5HlubPlj7BYQm9vdCDlvq7kv6HlsI/nqIvluo8gVW5pYXBwIOWJjeerryBWdWUgaVZpZXcgQWRtaW4g5ZCO56uv5YiG5biD5byP6ZmQ5rWBIOWQjOatpemUgSDpqozor4HnoIEgU25vd0ZsYWtl6Zuq6Iqx566X5rOVSUQg5Yqo5oCB5p2D6ZmQIOaVsOaNruadg+mZkCDlt6XkvZzmtYEg5Luj56CB55Sf5oiQIOWumuaXtuS7u+WKoSDnpL7kuqTotKblj7cg55+t5L+h55m75b2VIOWNleeCueeZu+W9lSBPQXV0aDLlvIDmlL7lubPlj7Ag5a6i5pyN5py65Zmo5Lq6IOaVsOaNruWkp+WxjyDmmpfpu5HmqKHlvI9HdW5z5Z+65LqOU3ByaW5nQm9vdCAyIOiHtOWKm+S6juWBmuabtOeugOa0geeahOWQjuWPsOeuoeeQhuezu+e7nyDlroznvo7mlbTlkIjpobnnm67ku6PnoIHnroDmtIEg5rOo6YeK5Liw5a+MIOS4iuaJi+WuueaYkyDlkIzml7ZHdW5z5YyF5ZCr6K645aSa5Z+656GA5qih5Z2XIOeUqOaIt+euoeeQhiDop5LoibLnrqHnkIYg6YOo6Zeo566h55CGIOWtl+WFuOeuoeeQhuetiTEg5Liq5qih5Z2XIOWPr+S7peebtOaOpeS9nOS4uuS4gOS4quWQjuWPsOeuoeeQhuezu+e7n+eahOiEmuaJi+aetiBBbmRyb2lkIOeJiOacrOabtOaWsOS4gOS4queugOa0geiAjOS8mOmbheeahEFuZHJvaWTljp/nlJ9VSeahhuaetiDop6PmlL7kvaDnmoTlj4zmiYsg5LiA5aWX5a6M5pW05pyJ5pWI55qEYW5kcm9pZOe7hOS7tuWMluaWueahiCDmlK/mjIHnu4Tku7bnmoTnu4Tku7blrozlhajpmpTnprsg5Y2V54us6LCD6K+VIOmbhuaIkOiwg+ivlSDnu4Tku7bkuqTkupIgVUnot7Povawg5Yqo5oCB5Yqg6L295Y246L29562J5Yqf6IO96YCC55So5LqOSmF2YeWSjEFuZHJvaWTnmoTlv6vpgJ8g5L2O5YaF5a2Y5Y2g55So55qE5rGJ5a2X6L2s5ou86Z+z5bqTIENvZGVzIG9mIG15IE1PT0MgQ291cnNlIFxcdTAwM2PmiJHlnKjmhZXor77nvZHkuIrnmoTor77nqIsg566X5rOV5LiO5pWw5o2u57uT5p6EIOekuuS+i+S7o+eggSDljIXmi6xDIOWSjEphdmHniYjmnKwg6K++56iL55qE5pu05aSa5pu05paw5YaF5a655Y+K6L6F5Yqp57uD5Lmg5Lmf5bCG6YCQ5q2l5re75Yqg6L+b6L+Z5Liq5Luj56CB5LuTIEhvcGUgQm9vdCDkuIDmrL7njrDku6PljJbnmoTohJrmiYvmnrbpobnnm67kuIDkuKrnroDljZXmvILkuq7nmoRTU00gU3ByaW5nIFNwcmluZ01WQyBNeWJhdGlzIOWNmuWuouezu+e7n+agueaNrkdzb27lupPkvb/nlKjnmoTopoHmsYIg5bCGSlNPTk9iamVjdOagvOW8j+eahFN0cmluZyDop6PmnpDmiJDlrp7kvZNC56uZIOWTlOWTqeWTlOWTqSBCaWxpYmlsaSDoh6rliqjnrb7liLDmipXluIHlt6Xlhbcg5q+P5aSp6L275p2+6I635Y+WNjXnu4/pqozlgLwg5pSv5oyB5q+P5pel6Ieq5Yqo5oqV5biBIOmTtueTnOWtkOWFkeaNouehrOW4gSDpooblj5blpKfkvJrlkZjnpo/liKkg5aSn5Lya5ZGY5pyI5bqV57uZ6Ieq5bex5YWF55S1562J5Yqf6IO9IOWRkCDotbblv6vlkozmiJHkuIDotbfmiJDkuLpMdjblkKcgSUpQYXkg6K6p5pSv5LuY6Kem5omL5Y+v5Y+KIOWwgeijheS6huW+ruS/oeaUr+S7mCBRUeaUr+S7mCDmlK/ku5jlrp3mlK/ku5gg5Lqs5Lic5pSv5LuYIOmTtuiBlOaUr+S7mCBQYXlQYWwg5pSv5LuY562J5bi455So55qE5pSv5LuY5pa55byP5Lul5Y+K5ZCE56eN5bi455So55qE5o6l5Y+jIOS4jeS+nei1luS7u+S9leesrOS4ieaWuSBtdmMg5qGG5p62IOS7heS7heS9nOS4uuW3peWFt+S9v+eUqOeugOWNleW/q+mAn+WujOaIkOaUr+S7mOaooeWdl+eahOW8gOWPkSDlj6/ovbvmnb7ltYzlhaXliLDku7vkvZXns7vnu5/ph4wg5Y+z5LiK6KeS54K55LiL5bCP5pif5pifIEhpZ2ggcXVhbGl0eSBwdXJlIFdlZXggZGVtbyDnvZHmmJPkuKXpgIkgQXBwIOaEn+WPlyBXZWV4IOW8gOWPkUFuZHJvaWQg5b+r6YCf5a6e546w5paw5omL5byV5a+85bGC55qE5bqTIOmAmui/h+eugOa0gemTvuW8j+iwg+eUqCDkuIDooYzku6PnoIHlrp7njrDlvJXlr7zlsYLnmoTmmL7npLrpgJrov4fmoIfnrb7nm7TmjqXnlJ/miJBzaGFwZSDml6DpnIDlho3lhplzaGFwZSB4bWwg5pys5bqT5piv5LiA5qy+5Z+65LqOUnhKYXZhMiBSZXRyb2ZpdDLlrp7njrDnroDljZXmmJPnlKjnmoTnvZHnu5zor7fmsYLmoYbmnrYg57uT5ZCIYW5kcm9pZOW5s+WPsOeJueaAp+eahOe9kee7nOWwgeijheW6kyDph4fnlKhhcGnpk77lvI/osIPnlKjkuIDngrnliLDlupUg6ZuG5oiQY29va2ll566h55CGIOWkmuenjee8k+WtmOaooeW8jyDmnoHnroBodHRwc+mFjee9riDkuIrkvKDkuIvovb3ov5vluqbmmL7npLog6K+35rGC6ZSZ6K+v6Ieq5Yqo6YeN6K+VIOivt+axguaQuuW4pnRva2VuIOaXtumXtOaIsyDnrb7lkI1zaWdu5Yqo5oCB6YWN572uIOiHquWKqOeZu+W9leaIkOWKn+WQjuivt+axgumHjeWPkeWKn+iDvSAz56eN5bGC5qyh55qE5Y+C5pWw6K6+572u6buY6K6k5YWo5bGA5bGA6YOoIOm7mOiupOagh+WHhkFwaVJlc3VsdOWQjOaXtuWPr+S7peaUr+aMgeiHquWumuS5ieeahOaVsOaNrue7k+aehCDlt7Lnu4/og73mu6HotrPnjrDlnKjnmoTlpKfpg6jliIbnvZHnu5zor7fmsYIgQW5kcm9pZCBCTEXok53niZnpgJrkv6HlupMg5Z+65LqORmxpbmvlrp7njrDnmoTllYblk4Hlrp7ml7bmjqjojZDns7vnu58gZmxpbmvnu5/orqHllYblk4Hng63luqYg5pS+5YWlcmVkaXPnvJPlrZgg5YiG5p6Q5pel5b+X5L+h5oGvIOWwhueUu+WDj+agh+etvuWSjOWunuaXtuiusOW9leaUvuWFpUhiYXNlIOWcqOeUqOaIt+WPkei1t+aOqOiNkOivt+axguWQjiDmoLnmja7nlKjmiLfnlLvlg4/ph43mjpLluo/ng63luqbmppwg5bm257uT5ZCI5Y2P5ZCM6L+H5ruk5ZKM5qCH562+5Lik5Liq5o6o6I2Q5qih5Z2X5Li65paw55Sf5oiQ55qE5qac5Y2V55qE5q+P5LiA5Liq5Lqn5ZOB5re75Yqg5YWz6IGU5Lqn5ZOBIOacgOWQjui/lOWbnuaWsOeahOeUqOaIt+WIl+ihqCDmkq3mlL7lmajln7rnoYDlupMg5LiT5rOo5LqO5pKt5pS+6KeG5Zu+57uE5Lu255qE6auY5aSN55So5oCn5ZKM57uE5Lu26Ze055qE5L2O6ICm5ZCIIOi9u+advuWkhOeQhuWkjeadguS4muWKoSDlm77niYfpgInmi6nlupMg5Y2V6YCJIOWkmumAiSDmi43nhacg6KOB5YmqIOWOi+e8qSDoh6rlrprkuYkg5YyF5ous6KeG6aKR6YCJ5oup5ZKM5b2V5Yi2IERhdGFY6ZuG5oiQ5Y+v6KeG5YyW6aG16Z2iIOmAieaLqeaVsOaNrua6kOWNs+WPr+S4gOmUrueUn+aIkOaVsOaNruWQjOatpeS7u+WKoSDmlK/mjIHnrYnmlbDmja7mupAg5om56YeP5Yib5bu6UkRCTVPmlbDmja7lkIzmraXku7vliqEg6ZuG5oiQ5byA5rqQ6LCD5bqm57O757ufIOaUr+aMgeWIhuW4g+W8jyDlop7ph4/lkIzmraXmlbDmja4g5a6e5pe25p+l55yL6L+Q6KGM5pel5b+XIOebkeaOp+aJp+ihjOWZqOi1hOa6kCBLSUxM6L+Q6KGM6L+b56iLIOaVsOaNrua6kOS/oeaBr+WKoOWvhuetiSBEZXByZWNhdGVkIGFuZHJvaWQg6Ieq5a6a5LmJ5pel5Y6G5o6n5Lu2IOaUr+aMgeW3puWPs+aXoOmZkOa7keWKqCDlkajmnIjliIfmjaIg5qCH6K6w5pel5pyf5pi+56S6IOiHquWumuS5ieaYvuekuuaViOaenOi3s+i9rOWIsOaMh+WumuaXpeacn+S4gOS4qumAmui/h+WKqOaAgeWKoOi9veacrOWcsOearuiCpOWMhei/m+ihjOaNouiCpOeahOearuiCpOahhuaetui/meaYr1JlZFNwaWRlcuekvuWMuuaIkOWRmOWOn+WIm+S4jue7tOaKpOeahEphdmHlpJrnur/nqIvns7vliJfmlofnq6Ag5LiA56uZ5byPQXBhY2hlIEthZmth6ZuG576k5oyH5qCH55uR5o6n5LiO6L+Q57u0566h5o6n5bmz5Y+w5b+r6YCf5byA5Y+R5bel5YW357G75pS26ZuGIOWPsuS4iuacgOWFqOeahOW8gOWPkeW3peWFt+exuyDmrKLov45Gb2xsb3cgRm9yayBTdGFy5ZCO56uv5oqA5pyv5oC757uTIOWMheaLrEphdmHln7rnoYAgSlZNIOaVsOaNruW6kyBteXNxbCByZWRpcyDorqHnrpfmnLrnvZHnu5wg566X5rOVIOaVsOaNrue7k+aehCDmk43kvZzns7vnu58g6K6+6K6h5qih5byPIOezu+e7n+iuvuiuoSDmoYbmnrbljp/nkIYg5pyA5L2z6ZiF6K+75Zyw5Z2AQW5kcm9pZOa6kOeggeiuvuiuoeaooeW8j+WIhuaekOmhueebruWPr+iDveaYr+acgOWlveeahOaUr+S7mFNESyDlgZzmraLnu7TmiqQg57uE5Lu25YyW57u85ZCI5qGI5L6LIOWMheWQq+W+ruS/oeaWsOmXuyDlpLTmnaHop4bpopEg576O5aWz5Zu+54mHIOeZvuW6pumfs+S5kCDlubLmtLvpm4bkuK3okKUg546pQW5kcm9pZCDosYbnk6Por7vkuabnlLXlvbEg55+l5LmO5pel5oql562J562J5qih5Z2XIOaetuaehOaooeW8jyDnu4Tku7bljJbpmL/ph4xWTGF5b3V0IOiFvuiur1g1IOiFvuiur2J1Z2x5IOiejeWQiOW8gOWPkeS4remcgOimgeeahOWQhOenjeWwj+ahiOS+iyDlvIDmupBPQeezu+e7nyDnoIHkupFHVlAgSmF2YeW8gOa6kG9hIOS8geS4mk9B5Yqe5YWs5bmz5Y+wIOS8geS4mk9BIOWNj+WQjOWKnuWFrE9BIOa1geeoi+W5s+WPsE9BIE8yT0EgT0Eg5pSv5oyB5Zu95Lqn6bqS6bqf5pON5L2c57O757uf5ZKM5Zu95Lqn5pWw5o2u5bqTIOi+vuaipiDkurrlpKfph5Hku5Mg5pS/5YqhT0Eg5Yab5bel5L+h5oGv5YyWT0Hku6VTcHJpbmcgQ2xvdWQgTmV0ZmxpeOS9nOS4uuacjeWKoeayu+eQhuWfuuehgCDlsZXnpLrln7rkuo50Y2PmgJ3mg7PmiYDlrp7njrDnmoTliIbluIPlvI/kuovliqHop6PlhrPmlrnmoYjkuIDkuKrluK7liqnmgqjlrozmiJDku47nvKnnlaXop4blm77liLDljp/op4blm77ml6DnvJ3ov4fmuKHovazlj5jnmoTnpZ7lpYfmoYbmnrYg57O757uf6YeN5p6E5LiO6L+B56e75oyH5Y2XIOaJi+aKiuaJi+aVmeS9oOWIhuaekCDor4TkvLDnjrDmnInns7vnu58g5Yi25a6a6YeN5p6E562W55WlIOaOoue0ouWPr+ihjOmHjeaehOaWueahiCDmkK3lu7rmtYvor5XpmLLmiqTnvZEg6L+b6KGM57O757uf5p625p6E6YeN5p6EIOacjeWKoeaetuaehOmHjeaehCDmqKHlnZfph43mnoQg5Luj56CB6YeN5p6EIOaVsOaNruW6k+mHjeaehCDph43mnoTlkI7nmoTmnrbmnoTlrojmiqTniYjmnKzmo4DmtYvljYfnuqcg5pu05pawIOW6k+Wwj+ivtOeyvuWTgeWxi+aYr+S4gOS4quWkmuW5s+WPsCB3ZWIg5a6J5Y2TYXBwIOW+ruS/oeWwj+eoi+W6jyDlip/og73lrozlloTnmoTlsY/luZXoh6rpgILlupTlsI/or7TmvKvnlLvov57ovb3ns7vnu58g5YyF5ZCr57K+5ZOB5bCP6K+05LiT5Yy6IOi9u+Wwj+ivtOS4k+WMuuWSjOa8q+eUu+S4k+WMuiDljIXmi6zlsI/or7Qg5ryr55S75YiG57G7IOWwj+ivtCDmvKvnlLvmkJzntKIg5bCP6K+0IOa8q+eUu+aOkuihjCDlrozmnKzlsI/or7Qg5ryr55S7IOWwj+ivtCDmvKvnlLvor4TliIYg5bCP6K+0IOa8q+eUu+WcqOe6v+mYheivuyDlsI/or7Qg5ryr55S75Lmm5p62IOWwj+ivtCDmvKvnlLvpmIXor7vorrDlvZUg5bCP6K+05LiL6L29IOWwj+ivtOW8ueW5lSDlsI/or7Qg5ryr55S76Ieq5Yqo6YeH6ZuGIOabtOaWsCDnuqDplJkg5bCP6K+05YaF5a656Ieq5Yqo5YiG5Lqr5Yiw5b6u5Y2aIOmCruS7tuiHquWKqOaOqOW5vyDpk77mjqXoh6rliqjmjqjpgIHliLDnmb7luqbmkJzntKLlvJXmk47nrYnlip/og70gQW5kcm9pZCDlvr3nq6Dmjqfku7Yg6Ie05Yqb5LqO5omT6YCg5LiA5qy+5p6B6Ie05L2T6aqM55qEIHd3dyB3YW5hbmRyb2lkIGNvbSDlrqLmiLfnq68g55+l6K+G5ZKM576O5piv5Y+v5Lul5bm25a2Y55qE5ZOmUUFRbiDiiacg4ommIG4g5LuO5rqQ56CB5bGC6Z2iIOWJluaekOaMluaOmOS6kuiBlOe9keihjOS4muS4u+a1geaKgOacr+eahOW6leWxguWunueOsOWOn+eQhiDkuLrlub/lpKflvIDlj5HogIUg4oCc5o+Q5Y2H5oqA5pyv5rex5bqm4oCdIOaPkOS+m+S+v+WIqSDnm67liY3lvIDmlL4gU3ByaW5nIOWFqOWutuahtiBNeWJhdGlzIE5ldHR5IER1YmJvIOahhuaetiDlj4ogUmVkaXMgVG9tY2F0IOS4remXtOS7tuetiVJlZGlzIOS4gOermeW8j+euoeeQhuW5s+WPsCDmlK/mjIHpm4bnvqTnmoTnm5Hmjqcg5a6J6KOFIOeuoeeQhiDlkYrorabku6Xlj4rln7rmnKznmoTmlbDmja7mk43kvZzor6Xpobnnm67kuI3lho3nu7TmiqQg5LuF5L6b5a2m5Lmg5Y+C6ICD5LiT5rOo5om56YeP5o6o6YCB55qE5bCP6ICM576O55qE5bel5YW3IOebruWJjeaUr+aMgSDmqKHmnb/mtojmga8g5YWs5LyX5Y+3IOaooeadv+a2iOaBryDlsI/nqIvluo8g5b6u5L+h5a6i5pyN5raI5oGvIOW+ruS/oeS8geS4muWPtyDkvIHkuJrlvq7kv6Hmtojmga8g6Zi/6YeM5LqR55+t5L+hIOmYv+mHjOWkp+S6juaooeadv+efreS/oSDohb7orq/kupHnn63kv6Eg5LqR54mH572R55+t5L+hIEUgTWFpbCBIVFRQ6K+35rGCIOmSiemSiSDljY7kuLrkupHnn63kv6Eg55m+5bqm5LqR55+t5L+hIOWPiOaLjeS6keefreS/oSDkuIPniZvkupHnn63kv6FBbmRyb2lkIOW5s+WPsOW8gOa6kOWkqeawlCBBcHAg6YeH55So562J5byA5rqQ5bqT5p2l5a6e546wIFNwcmluZ0Jvb3Qg55u45YWz5ryP5rSe5a2m5Lmg6LWE5paZIOWIqeeUqOaWueazleWSjOaKgOW3p+WQiOmbhiDpu5Hnm5Llronlhajor4TkvLAgY2hlY2sgbGlzdEFuZHJvaWQg5p2D6ZmQ6K+35rGC5qGG5p62IOW3sumAgumFjSBBbmRyb2lkIDEx5b6u5L+hU0RLIEpBVkEg5YWs5LyX5bmz5Y+wIOW8gOaUvuW5s+WPsCDllYbmiLflubPlj7Ag5pyN5Yqh5ZWG5bmz5Y+wIFFNUeaYr+WOu+WTquWEv+e9keWGhemDqOW5v+azm+S9v+eUqOeahOa2iOaBr+S4remXtOS7tiDoh6oyIDEy5bm06K+e55Sf5Lul5p2l5Zyo5Y675ZOq5YS/572R5omA5pyJ5Lia5Yqh5Zy65pmv5Lit5bm/5rOb55qE5bqU55SoIOWMheaLrOi3n+S6pOaYk+aBr+aBr+ebuOWFs+eahOiuouWNleWcuuaZryDkuZ/ljIXmi6zmiqXku7fmkJzntKLnrYnpq5jlkJ7lkJDph4/lnLrmma8gSmF2YSAyM+enjeiuvuiuoeaooeW8j+WFqOW9kue6s2xpbnV46L+Q57u055uR5o6n5bel5YW3IOaUr+aMgeezu+e7n+S/oeaBryDlhoXlrZggY3B1IOa4qeW6piDno4Hnm5jnqbrpl7Tlj4pJTyDnoaznm5hzbWFydCDns7vnu5/otJ/ovb0g572R57uc5rWB6YePIOi/m+eoi+etieebkeaOpyBBUEnmjqXlj6Mg5aSn5bGP5bGV56S6IOaLk+aJkeWbviDnq6/lj6Pnm5HmjqcgZG9ja2Vy55uR5o6nIOaXpeW/l+aWh+S7tuebkeaOpyDmlbDmja7lj6/op4bljJYgd2ViU1NI5bel5YW3IOWgoeWekuacuiDot7Pmnb/mnLog6L+Z5Y+v6IO95piv5YWo572R5pyA5aW955So55qEVmlld1BhZ2Vy6L2u5pKt5Zu+IOeugOWNlSDpq5jmlYgg5LiA6KGM5Luj56CB5a6e546w5b6q546v6L2u5pKtIOS4gOWxj+S4iemhteS7u+aEj+WPmCDmjIfnpLrlmajmoLflvI/ku7vkvaDmjJEg5LiA56eN566A5Y2V5pyJ5pWI55qEYW5kcm9pZOe7hOS7tuWMluaWueahiCDmlK/mjIHnu4Tku7bnmoTku6PnoIHotYTmupDpmpTnprsg5Y2V54us6LCD6K+VIOmbhuaIkOiwg+ivlSDnu4Tku7bkuqTkupIgVUnot7Povawg55Sf5ZG95ZGo5pyf562J5a6M5pW05Yqf6IO9IOS4gOS4quW8uuWkpyAxICUg5YW85a65IOaUr+aMgSBBbmRyb2lkWCDmlK/mjIEgS290bGlu5bm25LiU54G15rS755qE57uE5Lu25YyW5qGG5p62SlByZXNzIOS4gOS4quS9v+eUqCBKYXZhIOW8gOWPkeeahOW7uuermeelnuWZqCDnm67liY3lt7Lnu4/mnIkgMSB3IOe9keermeS9v+eUqCBKUHJlc3Mg6L+b6KGM6amx5YqoIOWFtuS4reWMheaLrOWkmuS4quaUv+W6nOacuuaehCAyIOS4iuW4guWFrOWPuCDkuK3np5HpmaIg57qiIOWtl+S8muetiSDliIbluIPlvI/kuovliqHmmJPnlKjnmoTovbvph4/ljJbnvZHnu5zniKzomasgQW5kcm9pZOezu+e7n+a6kOeggeWIhuaekOmHjeaehOS4reS4gOasvuWFjei0ueeahOaVsOaNruWPr+inhuWMluW3peWFtyDmiqXooajkuI7lpKflsY/orr7orqEg57G75Ly85LqOZXhjZWzmk43kvZzpo47moLwg5Zyo57q/5ouW5ou95a6M5oiQ5oql6KGo6K6+6K6hIOWKn+iDvea2teebliDmiqXooajorr7orqEg5Zu+5b2i5oql6KGoIOaJk+WNsOiuvuiuoSDlpKflsY/orr7orqHnrYkg5rC45LmF5YWN6LS5IOenieaJv+KAnOeugOWNlSDmmJPnlKgg5LiT5Lia4oCd55qE5Lqn5ZOB55CG5b+1IOaegeWkp+eahOmZjeS9juaKpeihqOW8gOWPkemavuW6piDnvKnnn63lvIDlj5HlkajmnJ8g6IqC55yB5oiQ5pysIOino+WGs+WQhOexu+aKpeihqOmavumimCBBbmRyb2lkIEFjdGl2aXR5IOa7keWKqOi/lOWbniDmlK/mjIHlvq7kv6Hmu5Hliqjov5Tlm57moLflvI8g5qiq5bGP5ruR5Yqo6L+U5ZueIOWFqOWxj+a7keWKqOi/lOWbnlNwcmluZ0Jvb3Qg5Z+656GA5pWZ56iLIOS7juWFpemXqOWIsOS4iueYviDln7rkuo4yIE015Yi25L2cIOS7v+W+ruS/oeinhumikeaLjeaRhFVJIOWfuuS6jmZmbXBlZ+eahOinhumikeW9leWItue8lui+kVB5dGhvbiAxIOWkqeS7juaWsOaJi+WIsOWkp+W4iCDliIbkuqsgR2l0SHViIOS4iuaciei2oyDlhaXpl6jnuqfnmoTlvIDmupDpobnnm67kuK3oi7HmlofmlY/mhJ/or40g6K+t6KiA5qOA5rWLIOS4reWkluaJi+acuiDnlLXor53lvZLlsZ7lnLAg6L+Q6JCl5ZWG5p+l6K+iIOWQjeWtl+aOqOaWreaAp+WIqyDmiYvmnLrlj7fmir3lj5Yg6Lqr5Lu96K+B5oq95Y+WIOmCrueuseaKveWPliDkuK3ml6XmlofkurrlkI3lupMg5Lit5paH57yp5YaZ5bqTIOaLhuWtl+ivjeWFuCDor43msYfmg4XmhJ/lgLwg5YGc55So6K+NIOWPjeWKqOivjeihqCDmmrTmgZDor43ooagg57mB566A5L2T6L2s5o2iIOiLseaWh+aooeaLn+S4reaWh+WPkemfsyDmsarls7DmrYzor43nlJ/miJDlmagg6IGM5Lia5ZCN56ew6K+N5bqTIOWQjOS5ieivjeW6kyDlj43kuYnor43lupMg5ZCm5a6a6K+N5bqTIOaxvei9puWTgeeJjOivjeW6kyDmsb3ovabpm7bku7bor43lupMg6L+e57ut6Iux5paH5YiH5YmyIOWQhOenjeS4reaWh+ivjeWQkemHjyDlhazlj7jlkI3lrZflpKflhagg5Y+k6K+X6K+N5bqTIElU6K+N5bqTIOi0oue7j+ivjeW6kyDmiJDor63or43lupMg5Zyw5ZCN6K+N5bqTIOWOhuWPsuWQjeS6uuivjeW6kyDor5for43or43lupMg5Yy75a2m6K+N5bqTIOmlrumjn+ivjeW6kyDms5Xlvovor43lupMg5rG96L2m6K+N5bqTIOWKqOeJqeivjeW6kyDkuK3mlofogYrlpKnor63mlpkg5Lit5paH6LCj6KiA5pWw5o2uIOeZvuW6puS4reaWh+mXruetlOaVsOaNrumbhiDlj6XlrZDnm7jkvLzluqbljLnphY3nrpfms5Xpm4blkIggYmVydOi1hOa6kCDmlofmnKznlJ/miJAg5pGY6KaB55u45YWz5bel5YW3IGNvY29OTFDkv6Hmga/mir3lj5YgMiAyMeW5tOacgOaWsOaAu+e7kyDpmL/ph4wg6IW+6K6vIOeZvuW6piDnvo7lm6Ig5aS05p2h562J5oqA5pyv6Z2i6K+V6aKY55uuIOS7peWPiuetlOahiCDkuJPlrrblh7rpopjkurrliIbmnpDmsYfmgLsgQWlMZWFybmluZyDmnLrlmajlrabkuaAgTWFjaGluZUxlYXJuaW5nIE1MIOa3seW6puWtpuS5oCBEZWVwTGVhcm5pbmcgREwg6Ieq54S26K+t6KiA5aSE55CGIE5MUDEyMyA25pm66IO95Yi356WoIOiuouelqOe7k+W3tOS4reaWh+WIhuivjSDliqjmiYvlrabmt7HluqblrabkuaAg6Z2i5ZCR5Lit5paH6K+76ICFIOiDvei/kOihjCDlj6/orqjorrog5Lit6Iux5paH54mI6KKr5YWo55CDMTc15omA5aSn5a2m6YeH55So5pWZ5a2mIOS4reaWh+WIhuivjSDor43mgKfmoIfms6gg5ZG95ZCN5a6e5L2T6K+G5YirIOS+neWtmOWPpeazleWIhuaekCDor63kuYnkvp3lrZjliIbmnpAg5paw6K+N5Y+R546wIOWFs+mUruivjeefreivreaPkOWPliDoh6rliqjmkZjopoEg5paH5pys5YiG57G76IGa57G7IOaLvOmfs+eugOe5gei9rOaNoiDoh6rnhLbor63oqIDlpITnkIblvq7kv6HkuKrkurrlj7fmjqXlj6Mg5b6u5L+h5py65Zmo5Lq65Y+K5ZG95Luk6KGM5b6u5L+hIOS4ieWNgeihjOWNs+WPr+iHquWumuS5ieS4quS6uuWPt+acuuWZqOS6uiDmlbDmja7nu5PmnoTlkoznrpfms5Xlv4Xnn6Xlv4XkvJrnmoQ1IOS4quS7o+eggeWunueOsEp1bXBTZXJ2ZXIg5piv5YWo55CD6aaW5qy+5byA5rqQ55qE5aCh5Z6S5py6IOaYr+espuWQiCA0QSDnmoTkuJPkuJrov5Dnu7TlronlhajlrqHorqHns7vnu58g6aOe5qGoIOaguOW/g+ahhuaetiDmt7HluqblrabkuaAg5py65Zmo5a2m5Lmg6auY5oCn6IO95Y2V5py6IOWIhuW4g+W8j+iuree7g+WSjOi3qOW5s+WPsOmDqOe9siDkuK3lm73nqIvluo/lkZjlrrnmmJPlj5Hpn7PplJnor6/nmoTljZXor43lvq7kv6Eg6Lez5LiA6LezIFB5dGhvbiDovoXliqkgcHl0aG9u5qih5ouf55m76ZmG5LiA5Lqb5aSn5Z6L572R56uZIOi/mOacieS4gOS6m+eugOWNleeahOeIrOiZqyDluIzmnJvlr7nkvaDku6zmnInmiYDluK7liqkg77iPIOWmguaenOWWnOasouiusOW+l+e7meS4qnN0YXLlk6Yg572R57uc54is6Jmr5a6e5oiYIOa3mOWunSDkuqzkuJwg572R5piT5LqRIELnq5kgMTIzIDYg5oqW6Z+zIOeslOi2o+mYgSDmvKvnlLvlsI/or7TkuIvovb0g6Z+z5LmQ55S15b2x5LiL6L29562JUHl0aG9u54is6Jmr5Luj55CGSVDmsaAgcHJveHkgcG9vbCB3dGZweXRob27nmoTkuK3mlofnv7vor5Eg5pa95bel57uT5p2fIOiDveWKm+aciemZkCDmrKLov47luK7miJHmlLnov5vnv7vor5Hmj5DkvpvlpJrmrL4gU2hhZG93cm9ja2V0IOinhOWImSDluKblub/lkYrov4fmu6Tlip/og70g55So5LqOIGlPUyDmnKrotorni7Horr7lpIfpgInmi6nmgKflnLDoh6rliqjnv7vlopkgMTIzIDYg6LSt56Wo5Yqp5omLIOaUr+aMgembhue+pCDlpJrotKblj7cg5aSa5Lu75Yqh6LSt56Wo5Lul5Y+KIFdlYiDpobXpnaLnrqHnkIYgd2FsbGUg55Om5YqbIERldm9wc+W8gOa6kOmhueebruS7o+eggemDqOe9suW5s+WPsOS4gOS6m+mdnuW4uOaciei2o+eahHB5dGhvbueIrOiZq+S+i+WtkCDlr7nmlrDmiYvmr5TovoPlj4vlpb0g5Li76KaB54is5Y+W5reY5a6dIOWkqeeMqyDlvq7kv6Eg6LGG55OjIFFR562J572R56uZ5py65Zmo5a2m5Lmg55u45YWz5pWZ56iLMSBDaGluZXNlIFdvcmQgVmVjdG9ycyDkuIrnmb7np43pooTorq3nu4PkuK3mlofor43lkJHph48g572R5piT5LqR6Z+z5LmQ5ZG95Luk6KGM54mI5pys5LiA5qy+5YWl6Zeo57qn55qE5Lq66IS4IOinhumikSDmloflrZfmo4DmtYvku6Xlj4ror4bliKvnmoTpobnnm64g57yW56iL6ZqP5oOzIOaVtOeQhueahCDlpKrlrZDlhZrlhbPns7vnvZHnu5wg5LiT6Zeo5o+t6Zyy6LW15Zu955qE5p2D6LS15b6u5L+h5Yqp5omLIDEg5q+P5pel5a6a5pe257uZ5aW95Y+LIOWls+WPiyDlj5HpgIHlrprliLbmtojmga8gMiDmnLrlmajkurroh6rliqjlm57lpI3lpb3lj4sgMyDnvqTliqnmiYvlip/og70g5L6L5aaCIOafpeivouWeg+WcvuWIhuexuyDlpKnmsJQg5pel5Y6GIOeUteW9seWunuaXtuelqOaIvyDlv6vpgJLnianmtYEgUE0yIDXnrYkg5LqM57u056CB55Sf5oiQ5ZmoIOaUr+aMgSBnaWYg5Yqo5oCB5Zu+54mH5LqM57u056CBIOmYv+W4g+mHj+WMluS6pOaYk+ezu+e7nyDogqHnpagg5pyf5p2DIOacn+i0pyDmr5TnibnluIEg5py65Zmo5a2m5LmgIOWfuuS6jnB5dGhvbueahOW8gOa6kOmHj+WMluS6pOaYkyDph4/ljJbmipXotYTmnrbmnoQgYm9vayDkuK3ljY7mlrDljY7lrZflhbjmlbDmja7lupMg5YyF5ous5q2H5ZCO6K+tIOaIkOivrSDor43or60g5rGJ5a2XIEdpdCBBV1MgR29vZ2xlIOmVnOWDjyBTUyBTU1IgVk1FU1PoioLngrnooYzkuJrnoJTnqbbmiqXlkYrnmoTnn6Xor4blgqjlpIflupPkuK3mlofnv7vor5HmiYvlhpnlrp7njrDmnY7oiKog57uf6K6h5a2m5Lmg5pa55rOVIOS5puS4reWFqOmDqOeul+azlSBQeXRob24g5oqW6Z+z5py65Zmo5Lq6IOiuuuWmguS9leWcqOaKlumfs+S4iuaJvuWIsOa8guS6ruWwj+WnkOWnkO+8nyDov4Hnp7vlrabkuaBweXRob27niKzomavmlZnnqIvns7vliJcg5LuOIOWIsDHlrabkuaBweXRob27niKzomasg5YyF5ous5rWP6KeI5Zmo5oqT5YyFIOaJi+acukFQUOaKk+WMhSDlpoIgZmlkZGxlciBtaXRtcHJveHkg5ZCE56eN54is6Jmr5raJ5Y+K55qE5qih5Z2X55qE5L2/55SoIOWmguetiSDku6Xlj4pJUOS7o+eQhiDpqozor4HnoIHor4bliKsgTXlzcWwgTW9uZ29EQuaVsOaNruW6k+eahHB5dGhvbuS9v+eUqCDlpJrnur/nqIvlpJrov5vnqIvniKzomavnmoTkvb/nlKggY3NzIOeIrOiZq+WKoOWvhumAhuWQkeegtOinoyBKU+eIrOiZq+mAhuWQkSDliIbluIPlvI/niKzomasg54is6Jmr6aG555uu5a6e5oiY5a6e5L6L562JUHl0aG9u6ISa5pysIOaooeaLn+eZu+W9leefpeS5jiDniKzomasg5pON5L2cZXhjZWwg5b6u5L+h5YWs5LyX5Y+3IOi/nOeoi+W8gOacuui2iuadpei2iuWkmueahOe9keermeWFt+acieWPjeeIrOiZq+eJueaApyDmnInnmoTnlKjlm77niYfpmpDol4/lhbPplK7mlbDmja4g5pyJ55qE5L2/55So5Y+N5Lq657G755qE6aqM6K+B56CBIOW7uueri+WPjeWPjeeIrOiZq+eahOS7o+eggeS7k+W6kyDpgJrov4fkuI7kuI3lkIznibnmgKfnmoTnvZHnq5nlgZrmlpfkuokg5peg5oG25oSPIOaPkOmrmOaKgOacryDmrKLov47mj5DkuqTpmr7ku6Xph4fpm4bnmoTnvZHnq5kg5Zug5bel5L2c5Y6f5ZugIOmhueebruaaguWBnCDkurrkurrlvbHop4Zib3Qg5a6M5YWo5a+55o6l5Lq65Lq65b2x6KeG5YWo6YOo5peg5Yig5YeP6LWE5rqQ6I6r54OmUHl0aG9uIOS4reaWh0FJ5pWZ5a2m6aOe5qGoIOWumOaWueaooeWei+W6kyDljIXlkKvlpJrnp43lrabmnK/liY3msr/lkozlt6XkuJrlnLrmma/pqozor4HnmoTmt7HluqblrabkuaDmqKHlnosg6L276YeP57qn5Lq66IS45qOA5rWL5qih5Z6LIOeZvuW6puS6kSDnmb7luqbnvZHnm5hQeXRob27lrqLmiLfnq68gUHl0aG9u6L+b6Zi2IEludGVybWVkaWF0ZSBQeXRob24g5Lit5paH54mIIOaPkOS+m+WQjOiKsemhuuWuouaIt+erryDlm73ph5Eg5Y2O5rOw5a6i5oi356uvIOmbqueQg+eahOWfuumHkSDogqHnpajoh6rliqjnqIvluo/ljJbkuqTmmJPku6Xlj4roh6rliqjmiZPmlrAg5pSv5oyB6Lef6LiqIGpvaW5xdWFudCByaWNlcXVhbnQg5qih5ouf5Lqk5piTIOWSjCDlrp7nm5jpm6rnkIPnu4TlkIgg6YeP5YyW5Lqk5piT57uE5Lu2UVVBTlRBWElTIOaUr+aMgeS7u+WKoeiwg+W6piDliIbluIPlvI/pg6jnvbLnmoQg6IKh56WoIOacn+i0pyDmnJ/mnYMg5riv6IKhIOiZmuaLn+i0p+W4gSDmlbDmja4g5Zue5rWLIOaooeaLnyDkuqTmmJMg5Y+v6KeG5YyWIOWkmui0puaItyDnuq/mnKzlnLDph4/ljJbop6PlhrPmlrnmoYhJTkZPIFNQSURFUiDmmK/kuIDkuKrpm4bkvJflpJrmlbDmja7mupDkuo7kuIDouqvnmoTniKzomavlt6XlhbfnrrEg5peo5Zyo5a6J5YWo5b+r5o2355qE5biu5Yqp55So5oi35ou/5Zue6Ieq5bex55qE5pWw5o2uIOW3peWFt+S7o+eggeW8gOa6kCDmtYHnqIvpgI/mmI4g5pSv5oyB5pWw5o2u5rqQ5YyF5ousR2l0SHViIFFR6YKu566xIOe9keaYk+mCrueusSDpmL/ph4zpgq7nrrEg5paw5rWq6YKu566xIEhvdG1haWzpgq7nrrEgT3V0bG9va+mCrueusSDkuqzkuJwg5reY5a6dIOaUr+S7mOWunSDkuK3lm73np7vliqgg5Lit5Zu96IGU6YCaIOS4reWbveeUteS/oSDnn6XkuY4g5ZOU5ZOp5ZOU5ZOpIOe9keaYk+S6kemfs+S5kCBRUeWlveWPiyBRUee+pCDnlJ/miJDmnIvlj4vlnIjnm7jlhowg5rWP6KeI5Zmo5rWP6KeI5Y6G5Y+yIDEyMyA2IOWNmuWuouWbrSBDU0RO5Y2a5a6iIOW8gOa6kOS4reWbveWNmuWuoiDnroDkuaYg5Lit5paHQkVSVCB3d23ns7vliJfmqKHlnosgUHl0aG9u5YWl6Zeo572R57uc54is6Jmr5LmL57K+5Y2O54mI5Lit5paHIGlPUyBNYWMg5byA5Y+R5Y2a5a6i5YiX6KGoUHl0aG9u572R6aG15b6u5L+hQVBJcGt1c2Vn5aSa6aKG5Z+f5Lit5paH5YiG6K+N5bel5YW36Ieq5bex5Yqo5omL5YGa6IGK5aSp5py65Zmo5Lq65pWZ56iL5Z+65LqO5pCc54uX5b6u5L+h5pCc57Si55qE5b6u5L+h5YWs5LyX5Y+354is6Jmr5o6l5Y+j55So5rex5bqm5a2m5Lmg5a+55a+56IGUIHYycmF5IHhyYXnlpJrnlKjmiLfnrqHnkIbpg6jnvbLnqIvluo/lkITnp43ohJrmnKwg5YWz5LqOIOiZvuexsyB4aWFtaSBjb20g55m+5bqm572R55uYIHBhbiBiYWlkdSBjb20gMTE1572R55uYIDExNSBjb20g572R5piT6Z+z5LmQIG11c2ljIDE2MyBjb20g55m+5bqm6Z+z5LmQIG11c2ljIGJhaWR1IGNvbSAzNiDnvZHnm5gg5LqR55uYIHl1bnBhbiBjbiDop4bpopHop6PmnpAgZmx2eHogY29tIGJ0IHRvcnJlbnQg77+8IG1hZ25ldCBlZDJrIOaQnOe0oiB0dW1ibHIg5Zu+54mH5LiL6L29IHVuemlw5p+l55yL6KKr5Yig55qE5b6u5L+h5aW95Y+L5a6a5oqV5pS55Y+Y5ZG96L+QIOiuqeaXtumXtOmZquS9oOaFouaFouWPmOWvjCBvbnJlZ3VsYXJpbnZlc3RpbmcgY29tIOacuuWZqOWtpuS5oOWunuaImCBQeXRob24zIGtOTiDlhrPnrZbmoJEg6LSd5Y+25pavIOmAu+i+keWbnuW9kiBTVk0g57q/5oCn5Zue5b2SIOagkeWbnuW9klN0YXRpc3RpY2FsIGxlYXJuaW5nIG1ldGhvZHMg57uf6K6h5a2m5Lmg5pa55rOVIOesrDLniYgg5p2O6IiqIOeslOiusCDku6PnoIEgbm90ZWJvb2sg5Y+C6ICD5paH54yuIEVycmF0YSBsaWhhbmcgc3RvY2sg6IKh56Wo57O757ufIOS9v+eUqHB5dGhvbui/m+ihjOW8gOWPkSDln7rkuo7mt7HluqblrabkuaDnmoTkuK3mlofor63pn7Por4bliKvns7vnu5/kuqzkuJzmiqLotK3liqnmiYsg5YyF5ZCr55m75b2VIOafpeivouWVhuWTgeW6k+WtmCDku7fmoLwg5re75YqgIOa4heepuui0reeJqei9piDmiqLotK3llYblk4Eg5LiL5Y2VIOafpeivouiuouWNleetieWKn+iDveiOq+eDplB5dGhvbiDkuK3mlodBSeaVmeWtpuacuuWZqOWtpuS5oOeul+azlXB5dGhvbuWunueOsOaWsOa1quW+ruWNmueIrOiZqyDnlKhweXRob27niKzlj5bmlrDmtarlvq7ljZrmlbDmja7nmoTnrpfms5Xku6Xlj4rpgJrnlKjnlJ/miJDlr7nmipfnvZHnu5zlm77lg4/nlJ/miJDnmoTnkIborrrkuI7lrp7ot7XnoJTnqbYg6Z2S5bKb5aSn5a2m5byA5rqQIE9ubGluZSBKdWRnZSBRUee+pCA0OTY3MSAxMjUgYWRtaW4gcWR1b2ogY29tV2VSb0JvdCDmmK/kuIDkuKrlvq7kv6HlhazkvJflj7flvIDlj5HmoYbmnrYg5Z+65LqORGphbmdv55qE5Y2a5a6i57O757ufIOS4reaWh+i/keS5ieivjSDogYrlpKnmnLrlmajkurog5pm66IO96Zeu562U5bel5YW35YyF5byA5rqQ6LSi57uP5pWw5o2u5o6l5Y+j5bqT5beh6aOO5piv5LiA5qy+6YCC55So5LqO5LyB5Lia5YaF572R55qE5ryP5rSe5b+r6YCf5bqU5oClIOW3oeiIquaJq+aPj+ezu+e7nyDnlarlj7flpKflhagg6Kej5Yaz55S16ISRIOaJi+acuueci+eUteinhuebtOaSreeahOiLpuaBvCDmlLbpm4blkITnp43nm7Tmkq3mupAg55S16KeG55u05pKt572R56uZ55+l6K+G5Zu+6LCx5p6E5bu6IOiHquWKqOmXruetlCDln7rkuo5rZ+eahOiHquWKqOmXruetlCDku6Xnlr7nl4XkuLrkuK3lv4PnmoTkuIDlrprop4TmqKHljLvoja/poobln5/nn6Xor4blm77osLEg5bm25Lul6K+l55+l6K+G5Zu+6LCx5a6M5oiQ6Ieq5Yqo6Zeu562U5LiO5YiG5p6Q5pyN5YqhIOWHuuWkhOacrOWcsOeUteW9seWIruWJiuS4juaVtOeQhuS4gOS9k+WMluino+WGs+aWueahiOiHquWKqOWMlui/kOe7tOW5s+WPsCBDTURCIENEIERldk9wcyDotYTkuqfnrqHnkIYg5Lu75Yqh57yW5o6SIOaMgee7reS6pOS7mCDns7vnu5/nm5Hmjqcg6L+Q57u0566h55CGIOmFjee9rueuoeeQhiB3dWtvbmcgcm9ib3Qg5piv5LiA5Liq566A5Y2VIOeBtea0uyDkvJjpm4XnmoTkuK3mlofor63pn7Plr7nor53mnLrlmajkurog5pm66IO96Z+z566x6aG555uuIOi/mOWPr+iDveaYr+mmluS4quaUr+aMgeiEkeacuuS6pOS6kueahOW8gOa6kOaZuuiDvemfs+eusemhueebriDojrflj5bmlpfpsbwg6JmO54mZIOWTlOWTqeWTlOWTqSDmipbpn7Mg5b+r5omL562JIDU1IOS4quebtOaSreW5s+WPsOeahOecn+Wunua1geWqkuS9k+WcsOWdgCDnm7Tmkq3mupAg5ZKM5by55bmVIOebtOaSrea6kOWPr+WcqCBQb3RQbGF5ZXIgZmx2IGpzIOetieaSreaUvuWZqOS4reaSreaUviDlrp3loZRMaW51eOmdouadvyDnroDljZXlpb3nlKjnmoTmnI3liqHlmajov5Dnu7TpnaLmnb/lhpzkuJrnn6Xor4blm77osLEgQWdyaUtHIOWGnOS4mumihuWfn+eahOS/oeaBr+ajgOe0oiDlkb3lkI3lrp7kvZPor4bliKsg5YWz57O75oq95Y+WIOaZuuiDvemXruetlCDovoXliqnlhrPnrZZDT0RP5piv5LiA5qy+5Li655So5oi35o+Q5L6b5LyB5Lia5aSa5re35ZCI5LqRIOS4gOermeW8j0Rldk9wcyDoh6rliqjljJbov5Dnu7Qg5a6M5YWo5byA5rqQ55qE5LqR566h55CG5bmz5Y+wIOiHquWKqOWMlui/kOe7tOW5s+WPsFdlYiBQZW50ZXN0aW5nIEZ1enog5a2X5YW4IOS4gOS4quWwseWkn+S6hiDorqHnrpfmnLrnvZHnu5wg6Ieq6aG25ZCR5LiL5pa55rOVIOWOn+S5puesrDbniYgg57yW56iL5L2c5LiaIFdpcmVzaGFya+WunumqjOaWh+aho+eahOe/u+ivkeWSjOino+etlCDkuK3mloflj6Tor5foh6rliqjkvZzor5fmnLrlmajkurog5bGM54K45aSpIOWfuuS6jnRlbnNvcmZsb3cxIDEgYXBpIOato+WcqOenr+aegee7tOaKpOWNh+e6p+S4rSDlv6tzdGFyIOS/neaMgeabtOaWsCBQeVF0IEV4YW1wbGVzIFB5UXTlkITnp43mtYvor5XlkozkvovlrZAgUHlRdDQgUHlRdDXmtbfph4/kuK3mlofpooTorq3nu4NBTEJFUlTmqKHlnovmsYnlrZfovazmi7zpn7MgcHlwaW55aW4g5pWw5o2u57uT5p6E5LiO566X5rOVIGxlZXRjb2RlIGxpbnRjb2Rl6aKY6KejIFB5dG9yY2jmqKHlnovorq3nu4Plrp7nlKjmlZnnqIsg5Lit6YWN5aWX5Luj56CB5a6e5pe26I635Y+W5paw5rWqIOiFvuiuryDnmoTlhY3otLnogqHnpajooYzmg4Ug6ZuG5oCd6Lev55qE5YiG57qn5Z+66YeR6KGM5oOFUHl0aG9u54is6JmrIEZsYXNr572R56uZIOWFjei0uVNoYWRvd1NvY2tz6LSm5Y+3IHNzcuiuoumYhSBqc29uIOiuoumYheWunuaImCDlpJrnp43nvZHnq5kg55S15ZWG5pWw5o2u54is6JmrIOWMheWQqyDmt5jlrp3llYblk4Eg5b6u5L+h5YWs5LyX5Y+3IOWkp+S8l+eCueivhCDkvIHmn6Xmn6Ug5oub6IGY572R56uZIOmXsumxvCDpmL/ph4zku7vliqEg5Y2a5a6i5ZutIOW+ruWNmiDnmb7luqbotLTlkKcg6LGG55Oj55S15b2xIOWMheWbvue9kSDlhajmma/nvZEg6LGG55Oj6Z+z5LmQIOafkOecgeiNr+ebkeWxgCDmkJzni5DmlrDpl7sg5py65Zmo5a2m5Lmg5paH5pys6YeH6ZuGIGZvZmHotYTkuqfph4fpm4Yg5rG96L2m5LmL5a62IOWbveWutue7n+iuoeWxgCDnmb7luqblhbPplK7or43mlLblvZXmlbAg6JyY6Jub5rOb55uu5b2VIOS7iuaXpeWktOadoSDosYbnk6PlvbHor4Qg5pC656iLIOWwj+exs+W6lOeUqOWVhuW6lyDlronlsYXlrqIg6YCU5a625rCR5a6/IO+4jyDvuI8g77iPIOW+ruS/oeeIrOiZq+WxleekuumhueebriBTUUwg5a6h5qC45p+l6K+i5bmz5Y+w5Zui5a2Q57+76K+R5ZmoIOS4quS6uuWFtOi2o+WItuS9nOeahOS4gOasvuWfuuS6jk9DUuaKgOacr+eahOe/u+ivkeWZqOiHquWKqOWMlui/kOe7tOW5s+WPsCDku6PnoIHlj4rlupTnlKjpg6jnvbJDSSBDRCDotYTkuqfnrqHnkIZDTURCIOiuoeWIkuS7u+WKoeeuoeeQhuW5s+WPsCBTUUzlrqHmoLgg5Zue5ruaIOS7u+WKoeiwg+W6piDnq5nlhoVXSUtJU291cmNlIENvZGUgU2VjdXJpdHkgQXVkaXQg5rqQ5Luj56CB5a6J5YWo5a6h6K6hIEV4cGh1YiDmvI/mtJ7liKnnlKjohJrmnKzlupMg5YyF5ous55qE5ryP5rSe5Yip55So6ISa5pysIOacgOaWsOa3u+WKoOaIkeeahOiHquWtpueslOiusCDnu4jouqvmm7TmlrAg5b2T5YmN5LiT5rOoU3lzdGVt5Z+656GAIE1MU3lzIOS9v+eUqOacuuWZqOWtpuS5oOeul+azleWujOaIkOWvuTEyMyA26aqM6K+B56CB55qE6Ieq5Yqo6K+G5YirUHl0aG9uIOW8gOa6kOmhueebruS5iyDoh6rlrabnvJbnqIvkuYvot68g5L+d5aeG57qn5pWZ56iLIEFJ5a6e6aqM5a6kIOWuneiXj+inhumikSDmlbDmja7nu5PmnoQg5a2m5Lmg5oyH5Y2XIOacuuWZqOWtpuS5oOWunuaImCDmt7HluqblrabkuaDlrp7miJgg572R57uc54is6JmrIOWkp+WOgumdoue7jyDnqIvluo/kurrnlJ8g6LWE5rqQ5YiG5LqrIOS4reaWh+aWh+acrOWIhuexu+WfuuS6jnB5dG9yY2gg5byA566x5Y2z55SoIOagueaNrue9keaYk+S6kemfs+S5kOeahOatjOWNlSDkuIvovb1mbGFj5peg5o2f6Z+z5LmQ5Yiw5pys5Zyw6IW+6K6v5LyY5Zu+6auY57K+5bqm5Y+M5YiG5pSv5Lq66IS45qOA5rWL5Zmo5paH5pys57qg6ZSZ562J5qih5Z6L5a6e546wIOW8gOeuseWNs+eUqCAzIOWkqeaOjOaPoemHj+WMluS6pOaYkyDmjIHnu63mm7TmlrAg5Lit5paH5YiG6K+NIOivjeaAp+agh+azqCDlkb3lkI3lrp7kvZPor4bliKsg5L6d5a2Y5Y+l5rOV5YiG5p6QIOaWsOivjeWPkeeOsCDlhbPplK7or43nn63or63mj5Dlj5Yg6Ieq5Yqo5pGY6KaBIOaWh+acrOWIhuexu+iBmuexuyDmi7zpn7PnroDnuYEg6Ieq54S26K+t6KiA5aSE55CG5Lit5paH5YWs5byA6IGK5aSp6K+t5paZ5bqT6LGG55Oj6K+75Lmm55qE54is6Jmr5oC757uT5qKz55CG6Ieq54S26K+t6KiA5aSE55CG5bel56iL5biIIE5MUCDpnIDopoHnp6/ntK/nmoTlkITmlrnpnaLnn6Xor4Yg5YyF5ous6Z2i6K+V6aKYIOWQhOenjeWfuuehgOefpeivhiDlt6XnqIvog73lipvnrYnnrYkg5o+Q5Y2H5qC45b+D56ue5LqJ5Yqb5Lit5paH6Ieq54S26K+t6KiA5aSE55CG5pWw5o2u6ZuGIOW5s+aXtuWBmuWBmuWunumqjOeahOadkOaWmSDmrKLov47ooaXlhYXmj5DkuqTlkIjlubYg5LiA5Liq5Y+v5Lul6Ieq5bex6L+b6KGM6K6t57uD55qE5Lit5paH6IGK5aSp5py65Zmo5Lq6IOagueaNruiHquW3seeahOivreaWmeiuree7g+WHuuiHquW3seaDs+imgeeahOiBiuWkqeacuuWZqOS6uiDlj6/ku6XnlKjkuo7mmbrog73lrqLmnI0g5Zyo57q/6Zeu562UIOaZuuiDveiBiuWkqeetieWcuuaZryDnm67liY3ljIXlkKtzZXEyc2VxIHNlcUdBTueJiOacrCB0ZjIg54mI5pysIHB5dG9yY2jniYjmnKwg6IKh56Wo6YeP5YyW5qGG5p62IOaUr+aMgeihjOaDheiOt+WPluS7peWPiuS6pOaYk+W+ruWNmueIrOiZqyDmjIHnu63nu7TmiqQgQmlsaWJpbGkg55So5oi354is6JmrIGRlZXBpbua6kOenu+akjSBEZWJpYW4gVWJ1bnR15LiK5pyA5b+r55qEUVEg5b6u5L+h5a6J6KOF5pa55byPIOaWsOmXu+e9kemhteato+aWh+mAmueUqOaKveWPluWZqCBCZXRhIOeJiCBmbGFnIG9uIHBvc3Qg6Ieq5Yqo5pu05paw5Z+f5ZCN6Kej5p6Q5Yiw5pys5py6SVAg5pSv5oyBZG5zcG9kIOmYv+mHjEROUyBDbG91ZEZsYXJlIOWNjuS4uuS6kSBETlNDT00g5pys6aG555uu6ZKI5a+55a2X56ym5Z6L5Zu+54mH6aqM6K+B56CBIOS9v+eUqHRlbnNvcmZsb3flrp7njrDljbfnp6/npZ7nu4/nvZHnu5wg6L+b6KGM6aqM6K+B56CB6K+G5YirIG93bGxvb2sg5bCP6K+05pCc57Si5byV5pOO5Lit5paH6K+t6KiA55CG6Kej5rWL6K+E5Z+65YeGcHl0aG9u5Lit5paH5bqTIHB5dGhvbuS6uuW3peaZuuiDveWkp+aVsOaNruiHquWKqOWMluaOpeWPo+a1i+ivleW8gOWPkSDkuabnsY3kuIvovb3lj4pweXRob27lupPmsYfmgLtjaGluYSB0ZXN0aW5nIGdpdGh1YiBpbyAyIDE55paw5Z6L5Yag54q255eF5q+S55ar5oOF5pe26Ze05bqP5YiX5pWw5o2u5LuT5bqTUHl0aG9uIOm7kemtlOazleaJi+WGjOWNlemYtuautemAmueUqOebruagh+ajgOa1i+WZqOS4gOS4quaLjeeFp+WBmumimOeoi+W6jyDovpPlhaXkuIDlvKDljIXlkKvmlbDlraborqHnrpfpopjnmoTlm77niYcg6L6T5Ye66K+G5Yir5Ye655qE5pWw5a2m6K6h566X5byP5Lul5Y+K6K6h566X57uT5p6cIHZpZGVvIGRvd25sb2FkIELnq5nop4bpopHkuIvovb3kuK3mloflkb3lkI3lrp7kvZPor4bliKsgVGVuc29yRmxvdyBQeXRob24g5Lit5paH5pWw5o2u57uT5p6E5ZKM566X5rOV5pWZ56iLIOmqjOivgeeggeivhuWIqyDorq3nu4NQeXRob27niKzomavlrp7miJgg5qih5ouf55m76ZmG5ZCE5aSn572R56uZIOWMheWQq+S9huS4jemZkOS6jiDmu5HlnZfpqozor4Eg5ou85aSa5aSaIOe+juWboiDnmb7luqYgYmlsaWJpbGkg5aSn5LyX54K56K+EIOa3mOWunSDlpoLmnpzllpzmrKLor7dzdGFydCDvuI/lrabml6DmraLkuIvovb3lmagg5oWV6K++5LiL6L295ZmoIE1vb2PkuIvovb0g5oWV6K++572R5LiL6L29IOS4reWbveWkp+WtpuS4i+i9vSDniLHor77nqIvkuIvovb0g572R5piT5LqR6K++5aCC5LiL6L29IOWtpuWgguWcqOe6v+S4i+i9vSDotoXmmJ/lrabkuaDpgJrkuIvovb0g5pSv5oyB6KeG6aKRIOivvuS7tuWQjOaXtuS4i+i9veS4gOS4qumrmOe6p3dlYuebruW9lSDmlofku7bmiavmj4/lt6Xlhbcg5Yqf6IO95bCG5Lya5by65LqORGlyQnVzdGVyIERpcnNlYXJjaCBjYW5zaW5hIOW+oeWJkSDmkJzntKLmiYDmnInkuK3mlodOTFDmlbDmja7pm4Yg6ZmE5bi455So6Iux5paHTkxQ5pWw5o2u6ZuG5Lit5paH5a6e5L2T6K+G5Yir5LiO5YWz57O75o+Q5Y+WMiAxOeaWsOWei+WGoOeKtueXheavkueWq+aDheWunuaXtueIrOiZq+WPimdpdGh1YiByZWxlYXNlIGFyY2hpdmXku6Xlj4rpobnnm67mlofku7bnmoTliqDpgJ/pobnnm67lronljZPlupTnlKjlronlhajlrabkuaDmipPlj5blpKfph4/lhY3otLnku6PnkIYgaXAg5o+Q5Y+W5pyJ5pWIIGlwIOS9v+eUqFJvQkVSVGHkuK3mlofpooTorq3nu4PmqKHlnosgUm9CRVJUYSBmb3IgQ2hpbmVzZSDnlKjkuo7orq3nu4PkuK3oi7Hmloflr7nor53ns7vnu5/nmoTor63mlpnlupPmlY/mhJ/or43ov4fmu6TnmoTlh6Dnp43lrp7njrAg5p+QMXfor43mlY/mhJ/or43lupPnroDljZXmmJPnlKjnmoRQeXRob27niKzomavmoYbmnrYgUVHkuqTmtYHnvqQgNTk3NTEgNTYg5L2/55SoQmVydCBFUk5JRSDov5vooYzkuK3mlofmlofmnKzliIbnsbvkuLogQ1NBUFAg6KeG6aKR6K++56iL5o+Q5L6b5a2X5bmVIOe/u+ivkSBQUFQgTGFiIFB5VG9yY2gg5a6Y5pa55Lit5paH5pWZ56iL5YyF5ZCrIDYg5YiG6ZKf5b+r6YCf5YWl6Zeo5pWZ56iLIOW8uuWMluaVmeeoiyDorqHnrpfmnLrop4bop4kg6Ieq54S26K+t6KiA5aSE55CGIOeUn+aIkOWvueaKl+e9kee7nCDlvLrljJblrabkuaAg5qyi6L+OIFN0YXIgRm9yayDlhZzlk6Xlh7rlk4EgXFx1MDAzY+S4gOacrOW8gOa6kOeahE5MUOWFpemXqOS5puexjSDlm77lg4/nv7vor5Eg5p2h5Lu2R0FOIEFJ57uY55S755SoUmVzbmV0MSAxIEdQVOaQreW7uuS4gOS4queOqeeOi+iAheiNo+iAgOeahEFJ5ZCE56eN5ryP5rSecG9jIEV4cOeahOaUtumbhuaIlue8luWGmeaWl+WcsOS4u0FJVnVsbWFwIOaYr+S4gOasviB3ZWIg5ryP5rSe5omr5o+P5ZKM6aqM6K+B5bel5YW3IOWPr+WvuSB3ZWJhcHBzIOi/m+ihjOa8j+a0nuaJq+aPjyDlubbkuJTlhbflpIfmvI/mtJ7pqozor4Hlip/og73mj5DkvpvotoXpgY4gNSDlgIvph5Hono3os4fmlpkg5Y+w6IKh54K65Li7IOavj+WkqeabtOaWsCBmaW5taW5kIGdpdGh1YiBpbyDmlbDmja7mjqXlj6Mg55m+5bqmIOiwt+atjCDlpLTmnaEg5b6u5Y2a5oyH5pWwIOWuj+inguaVsOaNriDliKnnjofmlbDmja4g6LSn5biB5rGH546HIOWNg+mHjOmprCDni6zop5Llhb3lhazlj7gg5paw6Ze76IGU5pKt5paH5a2X56i/IOW9seinhuelqOaIv+aVsOaNriDpq5jmoKHlkI3ljZUg55ar5oOF5pWw5o2uIFB5T25lIOS4gOasvue7meWKm+eahG9uZWRyaXZl5paH5Lu2566h55CGIOWIhuS6q+eoi+W6jyDkvb/nlKjlvIDlj5HnmoTnlKjkuo7ov4XpgJ/mkK3lu7rlubbkvb/nlKggV2ViSG9vayDov5vooYzoh6rliqjljJbpg6jnvbLlkozov5Dnu7Qg5pSv5oyBIEdpdGh1YiBHaXRMYWIgR29ncyBHaXRPc2Mg6Lef5oiR5LiA6LW35YaZTWFrZWZpbGXph43liLbniYggcHl0aG9u6Ieq5Yqo5YyW6L+Q57u0IOaKgOacr+S4juacgOS9s+Wunui3tSDkuabkuK3npLrkvovlj4rmoYjkvovmupDnoIHoh6rnhLbor63oqIDlpITnkIblrp7pqowgc291Z2915pWw5o2u6ZuGIFRGIElERiDmlofmnKzliIbnsbsg6IGa57G7IOivjeWQkemHjyDmg4XmhJ/or4bliKsg5YWz57O75oq95Y+W562J5b6u5L+h5YWs5LyX5bmz5Y+wIFB5dGhvbiDlvIDlj5HljIUgREVQUkVDQVRFRCBXZWJsb2dpY+S4gOmUrua8j+a0nuajgOa1i+W3peWFtyBWMSA1IOabtOaWsOaXtumXtCAyIDIgNzMg5a6M5aSH5LyY6ZuF55qE5b6u5L+h5YWs5LyX5Y+35o6l5Y+jIOWOn+eUn+aUr+aMgeWQjOatpSDljY/nqIvkvb/nlKgg5pys56iL5bqP5peo5Zyo5Li65a6J5YWo5bqU5oCl5ZON5bqU5Lq65ZGY5a+5TGludXjkuLvmnLrmjpLmn6Xml7bmj5Dkvpvkvr/liKkg5a6e546w5Li75py65L6nQ2hlY2tsaXN055qE6Ieq5Yqo5YWo6Z2i5YyW5qOA5rWLIOagueaNruajgOa1i+e7k+aenOiHquWKqOaVsOaNruiBmuWQiCDov5vooYzpu5HlrqLmlLvlh7vot6/lvoTmuq/mupAgUHlDaGFybSDkuK3mlofmjIfljZcg5a6J6KOFIOegtOinoyDmlYjnjocg5oqA5ben57G75Ly85oyJ6ZSu57K+54G155qE6byg5qCH6ZSu55uY5b2V5Yi25ZKM6Ieq5Yqo5YyW5pON5L2cIOaooeaLn+eCueWHu+WSjOmUruWFpUdQVDIgZm9yIENoaW5lc2UgY2hpdGNoYXQg55So5LqO5Lit5paH6Zey6IGK55qER1BUMuaooeWeiyDlrp7njrDkuoZEaWFsb0dQVOeahE1NSeaAneaDsyDkuK3ljY7kurrmsJHlhbHlkozlm73lm73lrrbmoIflh4YgR0IgVCAyMjYg6KGM5pS/5Yy65YiS5Luj56CB5Z+65LqOcHl0aG9u55qE6YeP5YyW5Lqk5piT5bmz5Y+w5Lit5paH6K+t6Z+z6K+G5Yir5Z+65LqOIE9uZUJvdCDmoIflh4bnmoQgUHl0aG9uIOW8guatpSBRUSDmnLrlmajkurrmoYbmnrZSZWFsIFdvcmxkIE1hc2tlZCBGYWNlIERhdGFzZXQg5Y+j572p5Lq66IS45pWw5o2u6ZuGIFZ1bGZvY3VzIOaYr+S4gOS4qua8j+a0numbhuaIkOW5s+WPsCDlsIbmvI/mtJ7njq/looMgZG9ja2VyIOmVnOWDjyDmlL7lhaXljbPlj6/kvb/nlKgg5byA566x5Y2z55SoIOiwt+atjCDnmb7luqYg5b+F5bqU5Zu+54mH5LiL6L29IOWfuuS6juaWuemdoueahOaDheaEn+WIhuaekCDkvb/nlKhQeVRvcmNo5a6e546wIOa3seW6puWtpuS5oOS4juiuoeeul+acuuinhuiniSDphY3lpZfku6PnoIFBUlTnjq/looPkuIvoh6rliqjljJbohLHlo7PmlrnmoYjliKnnlKjnvZHnu5zkuIrlhazlvIDnmoTmlbDmja7mnoTlu7rkuIDkuKrlsI/lnovnmoTor4HliLjnn6Xor4blm77osLEg55+l6K+G5bqT5Lit5paH6LWE5rqQ57K+6YCJIOWumOaWuee9keermSDlronoo4XmlZnnqIsg5YWl6Zeo5pWZ56iLIOinhumikeaVmeeoiyDlrp7miJjpobnnm64g5a2m5Lmg6Lev5b6EIFFR576kIDE2NzEyMjg2MSDlhazkvJflj7cg56OQ5YibQUkg5b6u5L+h576k5LqM57u056CBIHd3dyB0ZW5zb3JmbG93bmV3cyBjb20gUHJlIFRyYWluZWQgQ2hpbmVzZSBYTE5ldCDkuK3mlodYTE5ldOmihOiuree7g+aooeWeiyDmlrDmtarlvq7ljZpQeXRob24gU0RL5pyJ5YWzYnVycHN1aXRl55qE5o+S5Lu2IOmdnuWVhuW6lyDmlofnq6Dku6Xlj4rkvb/nlKjmioDlt6fnmoTmlLbpm4Yg5q2k6aG555uu5LiN5YaN5o+Q5L6bYnVycHN1aXRl56C06Kej5paH5Lu2IOWmgumcgOimgeivt+WcqOWNmuWuom1yeG4gbmV05LiL6L29UHl0aG9uM+e8luWGmeeahENNU+a8j+a0nuajgOa1i+ahhuaetuWfuuS6jmRqYW5nb+eahOW3peS9nOa1geW8leaTjiDlt6XljZUg77iPIOWTlOWTqeS6kSDkuI3mlK/mjIHku7vmhI/mlofku7bnmoTlhajpgJ/kuIrkvKDkuI7kuIvovb3lvq7kv6FTREsg5YyF5ous5b6u5L+h5pSv5LuYIOW+ruS/oeWFrOS8l+WPtyDlvq7kv6HnmbvpmYYg5b6u5L+h5raI5oGv5aSE55CG562J5Lit5paH6Ieq54S26K+t6KiA55CG6Kej5aCh5Z6S5py6IOS6keahjOmdouiHquWKqOWMlui/kOe7tCDlrqHorqEg5b2V5YOPIOaWh+S7tueuoeeQhiBzZnRw5LiK5LygIOWunuaXtuebkeaOpyDlvZXlg4/lm57mlL4g572R6aG154mIcnogc3rkuIrkvKDkuIvovb0g5Yqo5oCB5Y+j5LukIGRqYW5nb+i9rOaNouS4reWbveefpee9kSBDQUog5qC85byP5paH54yu5Li6IFBERiDkvZvns7vovazmjaIg5oiQ5Yqf5LiO5ZCmIOeahuaYr+eOhOWtpiBIYW5MUOS9nOiAheeahOaWsOS5piDoh6rnhLbor63oqIDlpITnkIblhaXpl6gg6K+m57uG56yU6K6wIOS4mueVjOiJr+W/g+S5i+S9nCDkuabkuK3kuI3mmK/mnq/nh6Xml6DlkbPnmoTlhazlvI/nvZfliJcg6ICM5piv55So55m96K+d6ZiQ6L+w55qE6YCa5L+X5piT5oeC55qE566X5rOV5qih5Z6LIOS7juWfuuacrOamguW/teWHuuWPkSDpgJDmraXku4vnu43kuK3mlofliIbor40g6K+N5oCn5qCH5rOoIOWRveWQjeWunuS9k+ivhuWIqyDkv6Hmga/mir3lj5Yg5paH5pys6IGa57G7IOaWh+acrOWIhuexuyDlj6Xms5XliIbmnpDov5nlh6DkuKrng63pl6jpl67popjnmoTnrpfms5Xljp/nkIbkuI7lt6XnqIvlrp7njrAgUHl0aG9uMyDnvZHnu5zniKzomavlrp7miJgg6YOo5YiG5ZCr6K+m57uG5pWZ56iLIOeMq+ecvCDohb7orq/op4bpopEg6LGG55OjIOeglOaLm+e9kSDlvq7ljZog56yU6Laj6ZiB5bCP6K+0IOeZvuW6pueDreeCuSBC56uZIENTRE4g572R5piT5LqR6ZiF6K+7IOmYv+mHjOaWh+WtpiDnmb7luqbogqHnpagg5LuK5pel5aS05p2hIOW+ruS/oeWFrOS8l+WPtyDnvZHmmJPkupHpn7PkuZAg5ouJ5Yu+IOaciemBkyB1bnNwbGFzaCDlrp7kuaDlg6cg5rG96L2m5LmL5a62IOiLsembhOiBlOebn+ebkuWtkCDlpKfkvJfngrnor4Qg6ZO+5a62IExQTOi1m+eoiyDlj7Dpo44g5qKm5bm76KW/5ri4IOmYtOmYs+W4iOiXj+WunemYgSDlpKnmsJQg54mb5a6i572RIOeZvuW6puaWh+W6kyDnnaHliY3mlYXkuosg55+l5LmOIFdpc2jlvq7kv6HlhazkvJflj7fmlofnq6DnmoTniKzomasgUHl0aG9uIFdlYuW8gOWPkeWunuaImCDkuabkuK3mupDnoIHkuIDnm7Tlj6/nlKjnmoRHb0FnZW50IOS8muWumuaXtuaJq+aPj+WPr+eUqOeahGdvb2dsZSBnYWUgaXAg5o+Q5L6b5Y+v6Ieq5Yqo5YyW6I635Y+WaXDov5DooYznmoTniYjmnKzlsYLliarmnp0g6YCa6YGT5Ymq5p6dIOefpeivhuiSuOmmjyDkuK3mloflkb3lkI3lrp7kvZPor4bliKsg5YyF5ous5aSa56eN5qih5Z6LIEhNTSBDUkYgQmlMU1RNIEJpTFNUTSBDUkbnmoTlhbfkvZPlrp7njrAgVGhlIFdheSB0byBHbyDkuK3mlofor5HmnKwg5Lit5paH5q2j5byP5ZCNIEdvIOWFpemXqOaMh+WNlyDosKLosKIgU29sdXRpb25zIHRvIExlZXRDb2RlIGJ5IEdvIDEgJSB0ZXN0IGNvdmVyYWdlIHJ1bnRpbWUgYmVhdHMgMSAlIExlZXRDb2RlIOmimOino+S4gOasvui9u+mHj+e6pyDpq5jmgKfog70g5Yqf6IO95by65aSn55qE5YaF572R56m/6YCP5Luj55CG5pyN5Yqh5ZmoIOaUr+aMgXRjcCB1ZHAgc29ja3M1IGh0dHDnrYnlh6DkuY7miYDmnInmtYHph4/ovazlj5Eg5Y+v55So5p2l6K6/6Zeu5YaF572R572R56uZIOacrOWcsOaUr+S7mOaOpeWPo+iwg+ivlSBzc2jorr/pl64g6L+c56iL5qGM6Z2iIOWGhee9kWRuc+ino+aekCDlhoXnvZFzb2NrczXku6PnkIbnrYnnrYkg5bm25bim5pyJ5Yqf6IO95by65aSn55qEd2Vi566h55CG56uvIEdv6K+t6KiA6auY57qn57yW56iLIOW8gOa6kOWbvuS5piDmtrXnm5ZDR08gR2/msYfnvJbor63oqIAgUlBD5a6e546wIFByb3RvYnVm5o+S5Lu25a6e546wIFdlYuahhuaetuWunueOsCDliIbluIPlvI/ns7vnu5/nrYnpq5jpmLbkuLvpopgg5a6M56i/IOaYr+S4gOS4qui3qOW5s+WPsOeahOW8uuWKoOWvhuaXoOeJueW+geeahOS7o+eQhui9r+S7tiDpm7bphY3nva4g566X5rOV5qih5p2/IOacgOenkeWtpueahOWIt+mimOaWueW8jyDmnIDlv6vpgJ/nmoTliLfpopjot6/lvoQg5L2g5YC85b6X5oul5pyJIOeZvuW6pue9keebmOS4jemZkOmAn+WuouaIt+erryBnb2xhbmcgcXQ1IOi3qOW5s+WPsOWbvuW9oueVjOmdouaYr2dvbGFuZ+WunueOsOeahOmrmOaAp+iDvWh0dHAgaHR0cHMgd2Vic29ja2V0IHRjcCBzb2NrczXku6PnkIbmnI3liqHlmagg5pSv5oyB5YaF572R56m/6YCPIOmTvuW8j+S7o+eQhiDpgJrorq/liqDlr4Yg5aSc6K+7IOmAmui/hyBiaWxpYmlsaSDlnKjnur/nm7Tmkq3nmoTmlrnlvI/liIbkuqsgR28g55u45YWz55qE5oqA5pyv6K+d6aKYIOavj+WkqeWkp+WutuWcqOW+ruS/oSB0ZWxlZ3JhbSBTbGFjayDkuIrlj4rml7bmsp/pgJrkuqTmtYHnvJbnqIvmioDmnK/or53popgg5pSv5oyB5aSa5a625LqR5a2Y5YKo55qE5LqR55uY57O757ufIOi/memHjOaYr+WGmeWNmuWuoueahOWcsOaWuSBIYWxmcm9zdCBGaWVsZCDlhrDpnJzkuYvlnLBMYW50ZXJu5a6Y5pa554mI5pys5LiL6L29IOiTneeBryDnv7vlopkg5Luj55CGIOenkeWtpuS4iue9kSDlpJbnvZEg5Yqg6YCf5ZmoIOair+WtkCDot6/nlLHln7rkuo5naW4gdnVl5pCt5bu655qE5ZCO5Y+w566h55CG57O757uf5qGG5p62IOmbhuaIkGp3dOmJtOadgyDmnYPpmZDnrqHnkIYg5Yqo5oCB6Lev55SxIOWIhumhteWwgeijhSDlpJrngrnnmbvlvZXmi6bmiKog6LWE5rqQ5p2D6ZmQIOS4iuS8oOS4i+i9vSDku6PnoIHnlJ/miJDlmagg6KGo5Y2V55Sf5oiQ5ZmoIOmAmueUqOW3peS9nOa1geetieWfuuehgOWKn+iDvSDkupTliIbpkp/kuIDlpZdDVVJE5YmN5ZCO56uv5Luj56CBIOebrlZVRTPniYjmnKzmraPlnKjph43mnoQg5qyi6L+OaXNzdWXlkoxwciDliIbluIPlvI/niKzomavnrqHnkIblubPlj7Ag5pSv5oyB5Lu75L2V6K+t6KiA5ZKM5qGG5p62R29sYW5n5qCH5YeG5bqTIOWvueS6jueoi+W6j+WRmOiAjOiogCDmoIflh4blupPkuI7or63oqIDmnKzouqvlkIzmoLfph43opoEg5a6D5aW95q+U5LiA5Liq55m+5a6d566xIOiDveS4uuWQhOenjeW4uOingeeahOS7u+WKoeaPkOS+m+WujOe+jueahOino+WGs+aWueahiCDku6XnpLrkvovpqbHliqjnmoTmlrnlvI/orrLop6NHb2xhbmfnmoTmoIflh4blupMg5aSp55SoR2/liqjmiYvlhpkg5LuO6Zu25a6e546w57O75YiX5piv5LiA5Liq6auY5oCn6IO95LiU5L2O5o2f6ICX55qEIGdvcm91dGluZSDmsaAg5pyJIOaciSDorr7orqHmqKHlvI8gR29sYW5n5a6e546wIOeglOejqOiuvuiuoeaooeW8jyDor7vkuabnrJTorrBHb2xhbmflrp7njrDnmoTln7rkuo5iZWVnb+ahhuaetueahOaOpeWPo+WcqOe6v+aWh+aho+euoeeQhuezu+e7n+mrmOaAp+iDveW8gOa6kFJUU1DmtYHlqpLkvZPmnI3liqHlmagg5Z+65LqOZ2/or63oqIDnoJTlj5Eg57u05oqk5ZKM5LyY5YyWIFJUU1DmjqjmqKHlvI/ovazlj5EgUlRTUOaLieaooeW8j+i9rOWPkSDmmK/kuIDkuKrpq5jmgKfog70g6L276YeP57qnIOmdnumYu+WhnueahOS6i+S7tumpseWKqCBHbyDnvZHnu5zmoYbmnrYg5Z+65LqOR2luIFZ1ZSBFbGVtZW50IFVJ55qE5YmN5ZCO56uv5YiG56a75p2D6ZmQ566h55CG57O757uf6ISa5omL5p62IOWMheWQq+S6hiDlpJrnp5/miLfnmoTmlK/mjIEg5Z+656GA55So5oi3566h55CG5Yqf6IO9IGp3dOmJtOadgyDku6PnoIHnlJ/miJDlmaggUkJBQ+i1hOa6kOaOp+WItiDooajljZXmnoTlu7og5a6a5pe25Lu75Yqh562JIDPliIbpkp/mnoTlu7roh6rlt7HnmoTkuK3lkI7lj7Dpobnnm64g5paH5qGj6JOd6bK45pm65LqR6YWN572u5bmz5Y+wIEJsdWVLaW5nIENNREIg5LuK5pel54Ot5qacIOS4gOS4quiOt+WPluWQhOWkp+eDremXqOe9keermeeDremXqOWktOadoeeahOiBmuWQiOe9keermSDkvb/nlKhHb+ivreiogOe8luWGmSDlpJrljY/nqIvlvILmraXlv6vpgJ/mipPlj5bkv6Hmga8g6aKE6KeIIG1vIGZpc2jkuIDmnaHlkb3ku6Tnprvnur/lronoo4Xpq5jlj6/nlKhrdWJlcm5ldGVzIDNtaW7oo4XlrowgNyBNIDEg5bm06K+B5LmmIOeUn+S6p+eOr+Wig+eos+WmguiAgeeLl+mYv+mHjOW3tOW3tOW8gOa6kOeahOS4gOasvueugOWNleaYk+eUqCDlip/og73lvLrlpKfnmoTmt7fmsozlrp7pqozms6jlhaXlt6XlhbcgR2/or63oqIDlm5vljYHkuoznq6Dnu48g6K+m57uG6K6y6L+wR2/or63oqIDop4TojIPkuI7or63ms5Xnu4boioLlj4rlvIDlj5HkuK3luLjop4HnmoTor6/ljLog6YCa6L+H56CU6K+75qCH5YeG5bqT562J57uP5YW45Luj56CB6K6+6K6h5qih5byPIOWQr+WPkeivu+iAhea3seWIu+eQhuino0dv6K+t6KiA55qE5qC45b+D5oCd57u0IOi/m+WFpUdv6K+t6KiA5byA5Y+R55qE5pu06auY6Zi25q61IO+4j+S4gOS4qui9u+W3p+eahOe9kee7nOa3t+a3huS7o+eQhiDln7rkuo5Hb2xhbmfovbvph4/nuqdUQ1Dlubblj5HmnI3liqHlmajmoYbmnrblrprml7bku7vliqHnrqHnkIbns7vnu59LdWJlT3BlcmF0b3Ig5piv5LiA5Liq5byA5rqQ55qE6L276YeP57qnIEt1YmVybmV0ZXMg5Y+R6KGM54mIIOS4k+azqOS6juW4ruWKqeS8geS4muinhOWIkiDpg6jnvbLlkozov5DokKXnlJ/kuqfnuqfliKvnmoQgSzhzIOmbhue+pCDmnKzns7vnu5/mmK/pm4blt6XljZXnu5/orqEg5Lu75Yqh6ZKp5a2QIOadg+mZkOeuoeeQhiDngbXmtLvphY3nva7mtYHnqIvkuI7mqKHniYjnrYnnrYnkuo7kuIDouqvnmoTlvIDmupDlt6XljZXns7vnu58g5b2T54S25Lmf5Y+v5Lul56ew5LmL5Li65bel5L2c5rWB5byV5pOOIOiHtOWKm+S6juWHj+Wwkei3qOmDqOmXqOS5i+mXtOeahOayn+mAmiDoh6rliqjku7vliqHnmoTmiafooYwg5o+Q5Y2H5bel5L2c5pWI546H5LiO5bel5L2c6LSo6YePIOWHj+WwkeS4jeW/heimgeeahOW3peS9nOmHj+S4juS6uuS4uuWHuumUmeeOhyBHb+WunueOsOeahFRyb2phbuS7o+eQhiDmlK/mjIHlpJrot6/lpI3nlKgg6Lev55Sx5Yqf6IO9IENETuS4rei9rCBTaGFkb3dzb2Nrc+a3t+a3huaPkuS7tiDlpJrlubPlj7Ag5peg5L6d6LWWIEdv6K+t5rOV5qCR5YWl6ZeoIOW8gOWQr+iHquWItue8lueoi+ivreiogOWSjOe8luivkeWZqOS5i+aXhSDlvIDmupDlhY3otLnlm77kuaYgR2/or63oqIDov5vpmLYg5o6M5o+h5oq96LGh6K+t5rOV5qCRIEdv6K+t6KiAQVNUIOWHueivreiogCDkuIDmrL7lj6/lhajlubPlj7Dov5DooYznmoTmtY/op4jlmajmlbDmja7lr7zlh7rop6Plr4blt6XlhbcgR29sYW5n55u45YWzIOWuoeeov+i/m+W6pjggJSBHb+ivreazlSBHb+W5tuWPkeaAneaDsyBHb+S4jndlYuW8gOWPkSBHb+W+ruacjeWKoeiuvuaWveetiUp1cGl0ZXLmmK/mlpfpsbzlvIDmupDnmoTpnaLlkJHmnI3liqHmsrvnkIbnmoRHb2xhbmflvq7mnI3liqHmoYbmnrZFbGFzdGljc2VhcmNoIOWPr+inhuWMlkRhc2hCb2FyZCDmlK/mjIFFc+ebkeaOpyDlrp7ml7bmkJzntKIgSW5kZXggdGVtcGxhdGXlv6vmjbfmm7/mjaLkv67mlLkg57Si5byV5YiX6KGo5L+h5oGv5p+l55yLIFNRTCBjb252ZXJ0cyB0byBEU0znrYkg5LuO6Zeu6aKY5YiH5YWlIOS4sui/niBHbyDor63oqIDnm7jlhbPnmoTmiYDmnInnn6Xor4Yg6J6N5Lya6LSv6YCaIGdvbGFuZyBkZXNpZ24gZ28gcXVlc3Rpb25zV2VDaGF0IFNESyBmb3IgR28g5b6u5L+hU0RLIOeugOWNlSDmmJPnlKggZ28gZmFzdGRmcyDmmK/kuIDkuKrnroDljZXnmoTliIbluIPlvI/mlofku7bns7vnu58g56eB5pyJ5LqR5a2Y5YKoIOWFt+acieaXoOS4reW/gyDpq5jmgKfog70g6auY5Y+v6Z2gIOWFjee7tOaKpOetieS8mOeCuSDmlK/mjIHmlq3ngrnnu63kvKAg5YiG5Z2X5LiK5LygIOWwj+aWh+S7tuWQiOW5tiDoh6rliqjlkIzmraUg6Ieq5Yqo5L+u5aSNIE1hc3RlcmluZyBHTyDkuK3mlofor5HmnKwg546p6L2sIEdPIOS6keWOn+eUn+S4lOaYk+eUqOeahOW6lOeUqOeuoeeQhuW5s+WPsCBHbyBXZWIg5Z+656GAIOaYr+S4gOWll+mSiOWvuSBHb29nbGUg5Ye65ZOB55qEIEdvIOivreiogOeahOinhumikeivremfs+aVmeeoiyDkuLvopoHpnaLlkJHlrozmiJAgR28g57yW56iL5Z+656GAIOaVmeeoi+WQjuW4jOacm+i/m+S4gOatpeS6huino+acieWFsyBHbyBXZWIg5byA5Y+R55qE5a2m5Lmg6ICFIOS4reaWh+WQjSDmgp/nqbogQVBJIOe9keWFsyDmmK/kuIDkuKrln7rkuo4gR29sYW5n5byA5Y+R55qE5b6u5pyN5Yqh572R5YWzIOiDveWkn+WunueOsOmrmOaAp+iDvSBIVFRQIEFQSSDovazlj5Eg5pyN5Yqh57yW5o6SIOWkmuenn+aIt+euoeeQhiBBUEkg6K6/6Zeu5p2D6ZmQ5o6n5Yi2562J55uu55qEIOaLpeacieW8uuWkp+eahOiHquWumuS5ieaPkuS7tuezu+e7n+WPr+S7peiHquihjOaJqeWxlSDlubbkuJTmj5Dkvpvlj4vlpb3nmoTlm77lvaLljJbphY3nva7nlYzpnaIg6IO95aSf5b+r6YCf5biu5Yqp5LyB5Lia6L+b6KGMIEFQSSDmnI3liqHmsrvnkIYg5o+Q6auYIEFQSSDmnI3liqHnmoTnqLPlrprmgKflkozlronlhajmgKcg6ZuG5ZCI5aSa5a62IEFQSSDnmoTmlrDkuIDku6Plm77luopNSVTor77nqIsgRGlzdHJpYnV0ZWQgU3lzdGVtcyDlrabkuaDlkoznv7vor5FHb+ivreiogOWco+e7j+S4reaWh+eJiCDlj6rmjqXmlLZQUiBJc3N1Zeivt+aPkOS6pOWIsGdvbGFuZyBjaGluYSBnb3BsIHpoIHRyb2phbuWkmueUqOaIt+euoeeQhumDqOe9sueoi+W6jyDmlK/mjIF3ZWLpobXpnaLnrqHnkIZCb29rU3RhY2sg5Z+65LqOTWluRG9jIOS9v+eUqEJlZWdv5byA5Y+R55qE5Zyo57q/5paH5qGj566h55CG57O757ufIOWKn+iDveexu+S8vEdpdGJvb2vlkoznnIvkupEgd2VpeGluIHdlY2hhdCDlvq7kv6HlhazkvJflubPlj7Ag5b6u5L+h5LyB5Lia5Y+3IOW+ruS/oeWVhuaIt+W5s+WPsCDlvq7kv6HmlK/ku5ggZ28gZ29sYW5nIHNkayDok53nnLzkupHnm5ggRXllYmx1ZSBDbG91ZCBTdG9yYWdlIOivreiogOmrmOaAp+iDvee8lueoiyBHbyDor63oqIDpmbfpmLEgR290Y2hhcyBUcmFwcyDkvb/nlKggWE1pbmQg6K6w5b2VIExpbnV4IOaTjeS9nOezu+e7nyDnvZHnu5wgQyBHb2xhbmcg5Lul5Y+K5pWw5o2u5bqT55qE5LiA5Lqb6K6+6K6hY3FodHRw55qEZ29sYW5n5a6e546wIOi9u+mHjyDljp/nlJ/ot6jlubPlj7AgbXFhbnTmmK/kuIDmrL7ln7rkuo5Hb2xhbmfor63oqIDnmoTnroDmtIEg6auY5pWIIOmrmOaAp+iDveeahOWIhuW4g+W8j+W+ruacjeWKoeahhuaetuWfuuS6jnJlYWN0IG5vZGUganMgZ2/lvIDlj5HnmoTlvq7llYbln44g5ZCr5b6u5L+h5bCP56iL5bqPIE1NIFdpa2kg5LiA5Liq6L276YeP57qn55qE5LyB5Lia55+l6K+G5YiG5Lqr5LiO5Zui6Zif5Y2P5ZCM6L2v5Lu2IOWPr+eUqOS6juW/q+mAn+aehOW7uuS8geS4miBXaWtpIOWSjOWboumYn+efpeivhuWIhuS6q+W5s+WPsCDpg6jnvbLmlrnkvr8g5L2/55So566A5Y2VIOW4ruWKqeWboumYn+aehOW7uuS4gOS4quS/oeaBr+WFseS6qyDmlofmoaPnrqHnkIbnmoTljY/kvZznjq/looMgR28g6K+t6KiA5Lit5paH572RIEdvbGFuZ+S4reaWh+ekvuWMuiBHb+ivreiogOWtpuS5oOWbreWcsCDmupDnoIHln7rkuo4gR2luIOi/m+ihjOaooeWdl+WMluiuvuiuoeeahCBBUEkg5qGG5p62IOWwgeijheS6huW4uOeUqOWKn+iDvSDkvb/nlKjnroDljZUg6Ie05Yqb5LqO6L+b6KGM5b+r6YCf55qE5Lia5Yqh56CU5Y+RIOavlOWmgiDmlK/mjIEgY29ycyDot6jln58gand0IOetvuWQjemqjOivgSB6YXAg5pel5b+X5pS26ZuGIHBhbmljIOW8guW4uOaNleiOtyB0cmFjZSDpk77ot6/ov73ouKogcHJvbWV0aGV1cyDnm5HmjqfmjIfmoIcgc3dhZ2dlciDmlofmoaPnlJ/miJAgdmlwZXIg6YWN572u5paH5Lu26Kej5p6QIGdvcm0g5pWw5o2u5bqT57uE5Lu2IGdvcm1nZW4g5Luj56CB55Sf5oiQ5bel5YW3IGdyYXBocWwg5p+l6K+i6K+t6KiAIGVycm5vIOe7n+S4gOWumuS5iemUmeivr+eggSBnUlBDIOeahOS9v+eUqCDnrYnnrYkgc3luY2TmmK/kuIDmrL7lvIDmupDnmoTku6PnoIHpg6jnvbLlt6Xlhbcg5a6D5YW35pyJ566A5Y2VIOmrmOaViCDmmJPnlKjnrYnnibnngrkg5Y+v5Lul5o+Q6auY5Zui6Zif55qE5bel5L2c5pWI546HIOS4gOasvueUsSBZU1JDIOW8gOa6kOeahOS4u+acuuWFpeS+teajgOa1i+ezu+e7n2dvbGFuZ+mdouivlemimOmbhuWQiOi/meaYr+S4gOS4quWPr+S7peivhuWIq+inhumikeivremfs+iHquWKqOeUn+aIkOWtl+W5lVNSVOaWh+S7tueahOW8gOa6kCBXaW5kb3dzIEdVSSDova/ku7blt6Xlhbcg5LiA5qy+5YaF572R57u85ZCI5omr5o+P5bel5YW3IOaWueS+v+S4gOmUruiHquWKqOWMliDlhajmlrnkvY3mvI/miavmiavmj48g5piv5LiA5Liq55So5LqO5Zyo5Lik5LiqcmVkaXPkuYvpl7TlkIzmraXmlbDmja7nmoTlt6Xlhbcg5ruh6Laz55So5oi36Z2e5bi454G15rS755qE5ZCM5q2lIOi/geenu+mcgOaxgiBPdmVybG9yZOaYr+WTlOWTqeWTlOWTqeWfuuS6jkdv6K+t6KiA57yW5YaZ55qEbWVtY2FjaGXlkoxyZWRpcyBjbHVzdGVy55qE5Luj55CG5Y+K6ZuG576k566h55CG5Yqf6IO9IOiHtOWKm+S6juaPkOS+m+iHquWKqOWMlumrmOWPr+eUqOeahOe8k+WtmOacjeWKoeino+WGs+aWueahiCBTdGFjayBSUEMg5Lit5paH56S65L6LIOaVmeeoiyDotYTmlpkg5rqQ56CB6Kej6K+7SUNNUOa1gemHj+S8quijhei9rOWPkeW3peWFt0ZyZWVkb23mmK/kuIDkuKrln7rkuo7lha3ovrnlvaLmnrbmnoTnmoTmoYbmnrYg5Y+v5Lul5pSv5pKR5YWF6KGA55qE6aKG5Z+f5qih5Z6L6IyD5byPIEdvMue8lueoi+aMh+WNlyDlvIDmupDlm77kuaYg6YeN54K56K6y6KejR28y5paw54m55oCnIOS7peWPikdvMeaVmeeoi+S4rei+g+Wwkea2ieWPiueahOeJueaAp+ivreiogOmrmOaAp+iDveWIhuivjWdvbGFuZ+WGmeeahElN5pyN5Yqh5ZmoIOacjeWKoee7hOS7tuW9ouW8jyDnu5Plt7Qg5Lit5paH5YiG6K+N55qER29sYW5n54mI5pyseG9ybeaYr+S4gOS4queugOWNleiAjOW8uuWkp+eahEdv6K+t6KiAT1JN5bqTIOmAmui/h+Wug+WPr+S7peS9v+aVsOaNruW6k+aTjeS9nOmdnuW4uOeugOS+vyDmnKzlupPmmK/ln7rkuo7ljp/niYh4b3Jt55qE5a6a5Yi25aKe5by654mI5pysIOS4unhvcm3mj5DkvpvnsbvkvLxpYmF0aXPnmoTphY3nva7mlofku7blj4rliqjmgIFTUUzmlK/mjIEg5pSv5oyBQWNpdHZlUmVjb3Jk5pON5L2c5LiA5LiqIEdvIOivreiogOWunueOsOeahOW/q+mAnyDnqLPlrpog5YaF5bWM55qEIGsgdiDmlbDmja7lupMg6auY5oCn6IO96KGo5qC85pWw5o2u5a+85Ye65Zmo5Z+65LqOR29sYW5n55qE5byA5rqQ56S+5Yy657O757ufIOeJiOacrOe9keaYk+S6kemfs+S5kG5jbeaWh+S7tuagvOW8j+i9rOaNoiBnbyDlrp7njrDnmoTljovmtYvlt6XlhbcgYWIgbG9jdXN0IEptZXRlcuWOi+a1i+W3peWFt+S7i+e7jSDljZXlj7DmnLrlmagxIHfov57mjqXljovmtYvlrp7miJgg5oqT5YyF5oiq5Y+W6aG555uu5Lit55qE5pWw5o2u5bqT6K+35rGC5bm26Kej5p6Q5oiQ55u45bqU55qE6K+t5Y+lIEdv5LiT5a6257yW56iLIEdv6K+t6KiA5b+r6YCf5YWl6ZeoIOi9u+advui/m+mYtiBcXHUwMDNjXFx1MDAzY+iHquW3seWKqOaJi+WGmWRvY2tlciDmupDnoIFHbyDmr4/ml6XkuIDlupNrdW5wZW5n5piv5LiA5LiqR29sYW5n57yW5YaZ55qE5byA5rqQUE9D5qGG5p62IOW6kyDku6XliqjmgIHpk77mjqXlupPnmoTlvaLlvI/mj5DkvpvlkITnp43or63oqIDosIPnlKgg6YCa6L+H5q2k6aG555uu5Y+v5b+r6YCf5byA5Y+R5ryP5rSe5qOA5rWL57G755qE57O757ufIHZ1ZSBqcyBlbGVtZW505qGG5p62IGdvbGFuZyBiZWVnb+ahhuaetiDlvIDlj5HnmoTov5Dnu7Tlj5HluIPns7vnu58g5pSv5oyBZ2l0IGplbmtpbnPniYjmnKzlj5HluIMgZ28gc3NoIEJU5Lik56eN5paH5Lu25Lyg6L6T5pa55byP6YCJ5oupIOaUr+aMgemDqOe9suWJjeWHhuWkh+S7u+WKoeWSjOmDqOe9suWQjuS7u+WKoemSqeWtkOWHveaVsCBHbyDku47lhaXpl6jliLDlrp7miJgg5a2m5Lmg56yU6K6wIOS7jumbtuW8gOWni+WtpiBHbyBHaW4g5qGG5p62IOWfuuacrOivreazleWMheaLrCAyNiDkuKpEZW1vIEdpbiDmoYbmnrbljIXmi6wgR2luIOiHquWumuS5iei3r+eUsemFjee9riBHaW4g5L2/55SoIExvZ3J1cyDov5vooYzml6Xlv5forrDlvZUgR2luIOaVsOaNrue7keWumuWSjOmqjOivgSBHaW4g6Ieq5a6a5LmJ6ZSZ6K+v5aSE55CGIEdvIGdSUEMgSGVsbG8gV29ybGQg5oyB57ut5pu05paw5LitIEdvIOWtpuS5oOS5i+i3ryBHbyDlvIDlj5HogIXljZrlrqIgR28g5b6u5L+h5YWs5LyX5Y+3IEdvIOWtpuS5oOi1hOaWmSDmlofmoaMg5Lmm57GNIOinhumikSDlvq7kv6EgV2VDaGF0IOaUr+S7mOWunSBBbGlQYXkg55qER2/niYjmnKxTREsg5p6B566AIOaYk+eUqOeahOiBmuWQiOaUr+S7mFNESyBHbyBieSBFeGFtcGxlIOmAmui/h+S+i+WtkOWtpiBHb2xhbmdQUEdvIEpvYuaYr+S4gOasvuWPr+inhuWMlueahCDlpJrkurrlpJrmnYPpmZDnmoQg5LiA5Lu75Yqh5aSa5py65omn6KGM55qE5a6a5pe25Lu75Yqh566h55CG57O757ufIOmHh+eUqGdvbGFuZ+W8gOWPkSDlronoo4Xmlrnkvr8g6LWE5rqQ5raI6ICX5bCRIOaUr+aMgeWkp+W5tuWPkSDlj6/lkIzml7bnrqHnkIblpJrlj7DmnI3liqHlmajkuIrnmoTlrprml7bku7vliqEgR29sYW5n5a6e546w55qESVDku6PnkIbmsaDmmK/kuIDmrL7nlKhHb+ivreiogOW8gOWPkeeahHdlYuW6lOeUqOahhuaetiBBUEnnibnmgKfnsbvkvLzkuo5Ub3JuYWRv5bm25LiU5oul5pyJ5q+UVG9ybmFkb+abtOWlveeahOaAp+iDvSDoh6rlt7HliqjmiYvlhplKYXZh6Jma5ouf5py6IOmaj+S5pua6kOS7o+eggeaUr+S7mOWunSBBbGlQYXkgU0RLIGZvciBHbyDpm4bmiJDnroDljZUg5Yqf6IO95a6M5ZaEIOaMgee7reabtOaWsCDmlK/mjIHlhazpkqXor4Hkuablkozmma7pgJrlhazpkqXov5vooYznrb7lkI3lkozpqoznrb4gQVJDSElWRUQgR2VwaCDov7fpnKfpgJrluK7liqnkvaDlsIbmnKzlnLDnq6/lj6PmmrTpnLLlnKjlpJbnvZEg5pSv5oyBVENQIFVEUCDlvZPnhLbkuZ/mlK/mjIFIVFRQIOa3seWFpUdv5bm25Y+R57yW56iL56CU6K6o6K++5peg54q25oCB5a2Q5Z+f5ZCN54iG56C05bel5YW35omL5py65Y+356CB5b2S5bGe5Zyw5L+h5oGv5bqTIOaJi+acuuWPt+W9kuWxnuWcsOafpeivoiBwaG9uZSBkYXQg5pyA5ZCO5pu05pawIDIgMjHlubQgNuaciCBnb2xhbmfln7rkuo53ZWJzb2NrZXTljZXlj7DmnLrlmajmlK/mjIHnmb7kuIfov57mjqXliIbluIPlvI/ogYrlpKkgSU0g57O757uf5Z+65LqObW9uZ29kYiBvcGxvZ+eahOmbhue+pOWkjeWItuW3peWFtyDlj6/ku6Xmu6HotrPov4Hnp7vlkozlkIzmraXnmoTpnIDmsYIg6L+b5LiA5q2l5a6e546w54G+5aSH5ZKM5aSa5rS75Yqf6IO9IEdpbiBHb3Jt5byA5Y+RR29sYW5nIEFQSeW/q+mAn+W8gOWPkeiEmuaJi+aetueugOWNleWPr+S/oei1lueahOS7u+WKoeeuoeeQhuW3peWFt0dv6K+t6KiA5a6e5L6L5pWZ56iL5LuO5YWl6Zeo5Yiw6L+b6Zi2IOWMheaLrOWfuuehgOW6k+S9v+eUqCDorr7orqHmqKHlvI8g6Z2i6K+V5piT6ZSZ54K5IOW3peWFt+exuyDlr7nmjqXnrKzkuInmlrnnrYnmjojmnYPmoYbmnrbnroDkvZPkuK3mlofnv7vor5Eg6Ieq5Yqo5oqT5Y+WdGfpopHpgZMg6K6i6ZiF5Zyw5Z2AIOWFrOW8gOS6kuiBlOe9keS4iueahHNzIHNzciB2bWVzcyB0cm9qYW7oioLngrnkv6Hmga8g6IGa5ZCI5Y676YeN5ZCO5o+Q5L6b6IqC54K55YiX6KGo6L276YeP57qnIGdvIOS4muWKoeahhuaetiDlk6rlkJLnm5Hmjqcg5LiA56uZ5byP6L2755uR5o6n6L276L+Q57u057O757ufIOaUr+aMgeezu+e7n+eKtuaAgSBUQ1AgUGluZyDnm5HmjqfmiqXoraYg5ZG95Luk5om56YeP5omn6KGM5ZKM6K6h5YiS5Lu75YqhIEdvIOivreiogOWumOaWueaVmeeoi+S4reaWh+eJiOW3peeoi+W4iOefpeivhueuoeeQhuezu+e7nyDln7rkuo5nb2xhbmcgZ2/or63oqIAgYmVlZ2/moYbmnrYg5q+P5Liq6KGM5Lia6YO95pyJ6Ieq5bex55qE55+l6K+G566h55CG57O757ufIGVuZ2luZWVyY21z5peo5Zyo5Li65Zyf5pyo5bel56iL5biI5Lus5omT6YCg5LiA5qy+6YCC55So55qE5Z+65LqOd2Vi55qE55+l6K+G566h55CG57O757ufIOWug+aXouWPr+S7peeUqOS6jueuoeeQhuS4quS6uueahOmhueebrui1hOaWmSDkuZ/lj6/ku6XnlKjkuo7nrqHnkIbpobnnm67lm6LpmJ/otYTmlpkg5a6D5pei5Y+v5Lul6L+Q6KGM5LqO5Liq5Lq655S16ISRIOS5n+WPr+S7peaUvuWIsOacjeWKoeWZqOS4iiDmlK/mjIHmj5Dlj5bnoIHliIbkuqvmlofku7Ygb25seW9mZmljZeWunuaXtuaWh+aho+WNj+S9nCDnm7TmjqXlnKjnur/nvJbovpFkd2fmlofku7Ygb2ZmaWNl5paH5qGjIOWcqOe6v+WIqeeUqG1pbmRvY+WIm+S9nOS9oOeahOS5puexjSDpmIXop4hQREbmlofku7Yg6YCa55So55qE5Lia5Yqh5rWB56iL6K6+572uIOaJi+acuuerr+mFjeWll+Wwj+eoi+W6jyDlvq7kv6HmkJzntKLigJznj6DkuInop5Lorr7ku6PigJ3miJbigJzpnZLlsJHlhL/kuabnlLvigJ3ljbPlj6/lkbzlh7rlsI/nqIvluo8g6L6555WM5omT54K55ZCO55qE6Ieq5Yqo5YyW5riX6YCP5bel5YW35LiA5Liq6ZuG5a6h5qC4IOaJp+ihjCDlpIfku73lj4rnlJ/miJDlm57mu5ror63lj6Xkuo7kuIDouqvnmoRNeVNRTOi/kOe7tOW3peWFt+axieWtl+i9rOaLvOmfsyBHb+i1hOa6kOeyvumAieS4reaWh+eJiCDlkKvkuK3mloflm77kuablpKflhagg6K+t6KiA5a6e546w55qEIFJlZGlzIOacjeWKoeWZqOWSjOWIhuW4g+W8j+mbhue+pCDotoXlhahnb2xhbmfpnaLor5XpopjlkIjpm4YgZ29sYW5n5a2m5Lmg5oyH5Y2XIGdvbGFuZ+efpeivhuWbvuiwsSDlhaXpl6jmiJDplb/ot6/nur8g5LiA5Lu95ra155uW5aSn6YOo5YiGZ29sYW5n56iL5bqP5ZGY5omA6ZyA6KaB5o6M5o+h55qE5qC45b+D55+l6K+GIOW4uOeUqOesrOS4ieaWueW6kyBteXNxbCBtcSBlcyByZWRpc+etiSDmnLrlmajlrabkuaDlupMg566X5rOV5bqTIOa4uOaIj+W6kyDlvIDmupDmoYbmnrYg6Ieq54S26K+t6KiA5aSE55CGbmxw5bqTIOe9kee7nOW6kyDop4bpopHlupMg5b6u5pyN5Yqh5qGG5p62IOinhumikeaVmeeoiyDpn7PpopHpn7PkuZDlupMg5Zu+5b2i5Zu+54mH5bqTIOeJqeiBlOe9keW6kyDlnLDnkIbkvY3nva7kv6Hmga8g5bWM5YWl5byP6ISa5pys5bqTIOe8luivkeWZqOW6kyDmlbDmja7lupMg6YeR6J6N5bqTIOeUteWtkOmCruS7tuW6kyDnlLXlrZDkuabnsY0g5YiG6K+NIOaVsOaNrue7k+aehCDorr7orqHmqKHlvI8g5Y67aHRtbCB0YWfmoIfnrb7nrYkgZ2/lrabkuaAgZ2/pnaLor5Vnb+ivreiogOaJqeWxleWMhSDmlLbpm4bkuIDkupvluLjnlKjnmoTmk43kvZzlh73mlbAg6L6F5Yqp5pu05b+r55qE5a6M5oiQ5byA5Y+R5bel5L2cIOW5tuWHj+WwkemHjeWkjeS7o+eggeeZvueBteW/q+S8oCDln7rkuo5Hb+ivreiogOeahOmrmOaAp+iDvSDmiYvmnLrnlLXohJHotoXlpKfmlofku7bkvKDovpPnpZ7lmagg5bGA5Z+f572R5YWx5Lqr5paH5Lu25pyN5Yqh5ZmoIExBTiBsYXJnZSBmaWxlIHRyYW5zZmVyIHRvb2wg5LiA5Liq5Z+65LqO5LqR5a2Y5YKo55qE572R55uY57O757ufIOeUqOS6juiHquW7uuengeS6uue9keebmOaIluS8geS4mue9keebmCBnb+WIhuW4g+W8j+acjeWKoeWZqCDln7rkuo7lhoXlrZhtbW/kuKrkurrljZrlrqLlvq7kv6HlsI/nqIvluo/mnI3liqHnq68gU0RLIGZvciBHb2xhbmcg5o6n5Yi25Y+w6aKc6Imy5riy5p+T5bel5YW35bqTIOaUr+aMgTE26ImyIDI1NuiJsiBSR0LoibLlvanmuLLmn5PovpPlh7og5L2/55So57G75Ly85LqOIFByaW50IFNwcmludGYg5YW85a655bm25pSv5oyBIFdpbmRvd3Mg546v5aKD55qE6Imy5b2p5riy5p+T5Z+65LqOIElvQyDnmoQgR28g5ZCO56uv5LiA56uZ5byP5byA5Y+R5qGG5p62IHYycmF5IHdlYiBtYW5hZ2VyIOaYr+S4gOS4qnYycmF555qE6Z2i5p2/IOS5n+aYr+S4gOS4qumbhue+pOeahOino+WGs+aWueahiCDlkIzml7blop7liqDkuobmtYHph4/mjqfliLYg6LSm5Y+3566h55CGIOmZkOmAn+etieWKn+iDvSBrZXkgYWRtaW4gcGFuZWwgd2ViIGNsdXN0ZXIg6ZuG576kIHByb3h5U2VydmVyU2NhbuS4gOasvuS9v+eUqEdvbGFuZ+W8gOWPkeeahOmrmOW5tuWPkee9kee7nOaJq+aPjyDmnI3liqHmjqLmtYvlt6Xlhbcg5pivaHR0cCBjbGllbnTpoobln5/nmoTnkZ7lo6vlhpvliIAg5bCP5benIOW8uuWkpyDnioDliKkg5YW35L2T55So5rOV5Y+v55yL5paH5qGjIOWmguS9v+eUqOi/t+aDkeaIluiAhUFQSeeUqOW+l+S4jeeIvemDveWPr+aPkGlzc3Vlc1RjcFJvdXRlIFRDUCDlsYLnmoTot6/nlLHlmagg5a+55LqOIFRDUCDov57mjqXoh6rliqjku47lpJrkuKrnur/ot68g55S15L+hIOiBlOmAmiDnp7vliqgg5aSa5Liq5Z+f5ZCN6Kej5p6Q57uT5p6c5Lit6YCJ5oup5pyA5LyY57q/6LevIEJpZnJvc3Qg6Z2i5ZCR55Sf5Lqn546v5aKD55qEIE15U1FMIOWQjOatpeWIsFJlZGlzIE1vbmdvREIgQ2xpY2tIb3VzZSBNeVNRTOetieacjeWKoeeahOW8guaehOS4remXtOS7tuW6lOeUqOe9keWFsyDmj5Dkvpvlv6vpgJ8g5a6J5YWo55qE5bqU55So5Lqk5LuYIOi6q+S7veiupOivgSBXQUYgQ0MgSFRUUFPku6Xlj4pBQ01F6Ieq5Yqo6K+B5LmmIEEgdGVsZWdyYW0gYm90IGZvciByc3MgcmVhZGVyIOS4gOS4quaUr+aMgeW6lOeUqOWGhemYheivu+eahCBUZWxlZ3JhbSBSU1MgQm90IFJFU1RmdWwgQVBJIOaWh+aho+eUn+aIkOW3peWFtyDmlK/mjIHlkowgUnVieSDnrYnlpKfpg6jliIbor63oqIAg5Z+65LqOZ2luIGdvcm3lvIDlj5HnmoTkuKrkurrljZrlrqLpobnnm67ln7rkuo5Hb+ivreiogOeahOWbveWvhlNNMiBTTTMgU000566X5rOV5bqTIEdvbGFuZyDorr7orqHmqKHlvI/kuIDkuKrpmL/ph4zkupHnm5jliJfooajnqIvluo8g5LiA5qy+5bCP5ben55qE5Z+65LqOR2/mnoTlu7rnmoTlvIDlj5HmoYbmnrYg5Y+v5Lul5b+r6YCf5p6E5bu6QVBJ5pyN5Yqh5oiW6ICFV2Vi572R56uZ6L+b6KGM5Lia5Yqh5byA5Y+RIOmBteW+qlNPTElE6K6+6K6h5Y6f5YiZ5bm25Y+R57yW56iL5a6e5oiYIOesrDLniYggR28g5a2m5LmgIEdvIOi/m+mYtiBHbyDlrp7nlKjlt6XlhbfnsbsgR28ga2l0IEdvIE1pY3JvIOW+ruacjeWKoeWunui3tSBHbyDmjqjpgIHln7rkuo5ERETnmoRvMm/nmoTkuJrliqHmqKHlnovlj4rln7rnoYAg5L2/55SoR29sYW5nIGdSUEMgVGhyaWZ05a6e546wU2hhcmluZ2FuIOWGmei9ruecvCDmmK/kuIDkuKrln7rkuo5nb2xhbmfnmoTmtYHph4/lvZXliLblm57mlL7lt6Xlhbcg6YCC5ZCI6aG555uu6YeN5p6EIOWbnuW9kua1i+ivleetiSDnmb7luqbkupHnvZHnm5jniKzomavln7rkuo5iZWVnb+eahOi/m+mUgOWtmOezu+e7nyBUZWFXZWIg5Y+v6KeG5YyW55qEV2Vi5Luj55CG5pyN5YqhIERFTU8gdGVhb3MgY24g55m95bi95a2Q5a6J5YWo5byA5Y+R5a6e5oiYIOmFjeWll+S7o+eggeaKlumfs+aOqOiNkCDmkJzntKLpobXop4bpopHliJfooajop4bpopHniKzomavmlrnmoYgg5Z+65LqOYXBwIOiZmuaLn+acuuaIluecn+acuiDnm7jlhbPmioDmnK8gZ29sYW5nIGFkYuS4gOasvueUsuaWuei1hOS6p+W3oeiIquaJq+aPj+ezu+e7nyDns7vnu5/lrprkvY3mmK/lj5HnjrDotYTkuqcg6L+b6KGM56uv5Y+j54iG56C0IOW4ruWKqeS8geS4muabtOW/q+WPkeeOsOW8seWPo+S7pOmXrumimCDkuLvopoHlip/og73ljIXmi6wg6LWE5Lqn5o6i5rWLIOerr+WPo+eIhuegtCDlrprml7bku7vliqEg566h55CG5ZCO5Y+w6K+G5YirIOaKpeihqOWxleekuuaPkOS+m+W+ruS/oee7iOerr+eJiOacrCDlvq7kv6Hlkb3ku6TooYzniYjmnKzogYrlpKnlip/og70g5b6u5L+h5py65Zmo5Lq6IO+4jyDkupLogZTnvZHmnIDlhajlpKfljoLmioDmnK/liIbkuqtQUFQg5oyB57ut5pu05paw5LitIOWQhOWkp+aKgOacr+S6pOa1geS8miDmtLvliqjotYTmlpnmsYfmgLsg5aaCIFFDb24g5YWo55CD6L+Q57u05oqA5pyv5aSn5LyaIEdERyDlhajnkIPmioDmnK/pooblr7zlipvls7DkvJog5aSn5YmN56uv5aSn5LyaIOaetuaehOW4iOWzsOS8miDmlY/mjbflvIDlj5FEZXZPcHMgT3BlblJlc3R5IEVsYXN0aWMg5qyi6L+OIFBSIElzc3Vlc+aXpeacrOm6u+WwhuWKqeaJiyDniYzmlYgg6Ziy5a6IIOiusOeJjCDmlK/mjIHpm4DprYIg5aSp5YekIOW8gOa6kOWuouacjeezu+e7n0dP6K+t6KiA5byA5Y+RR08gRkxZIOWFjei0ueWuouacjeezu+e7n+S4gOS4quafpeivoklQ5Zyw55CG5L+h5oGv5ZKMQ0RO5pyN5Yqh5o+Q5L6b5ZWG55qE56a757q/57uI56uv5bel5YW3IOaYr+S4gOS4queUqOS6juezu+e7n+mHjeaehCDns7vnu5/ov4Hnp7vlkozns7vnu5/liIbmnpDnmoTnkZ7lo6vlhpvliIAg5a6D5Y+v5Lul5YiG5p6Q5Luj56CB5Lit55qE5rWL6K+V5Z2P5ZGz6YGTIOaooeWdl+WMluWIhuaekCDooYzmlbDnu5/orqEg5YiG5p6Q6LCD55So5LiO5L6d6LWWIEdpdCDliIbmnpDku6Xlj4roh6rliqjljJbph43mnoTnrYkg5LiA5Liq55u05pKt5b2V5Yi25bel5YW3TWFzdGVyaW5nIEdvIOesrOS6jOeJiOS4reaWh+eJiOadpeiirSDmuJfpgI/mtYvor5Xmg4XmiqXmlLbpm4blt6XlhbfliIbluIPlvI/lrprml7bku7vliqHosIPluqblubPlj7Dpq5jluqbmqKHlnZfljJYg6YG15b6qIEtJU1Pljp/liJnnmoTljLrlnZfpk77lvIDlj5HmoYbmnrZnb2xhbmfniYjmnKznmoRoYW5nb3V0IOW4jOacm+iDveecgeS6m+WGheWtmCDkvb/nlKjkuoboh6rlt7HlhpnnmoRLYWZrYSBsaWIg6JmaIOS4jei/h+aIkeS7rOWcqOeUn+S6p+eOr+Wig+W3sue7j+S9v+eUqOi/kTHlubQga2Fma2Eg54mI5pys5LuOIDkgMeWIsDIg6YO95Zyo5L2/55SoIOebruWJjeaDheWGteeos+WumiDlkJ7lkJDph4/lnKjmr4/lpKkyIOS6v+adoeS7peS4iiBHbyDor63oqIAgV2ViIOW6lOeUqOW8gOWPkeezu+WIl+aVmeeoiyDku47mlrDmiYvliLDlj4zmiYvmrovlup9pcmlzIOahhuaetueahOWQjuWPsGFwaemhueebrueugOWNleWlveeUqOeahERETlMg6Ieq5Yqo5pu05paw5Z+f5ZCN6Kej5p6Q5Yiw5YWs572RSVAg5pSv5oyB6Zi/6YeM5LqRIOiFvuiur+S6kWRuc3BvZCBDbG91ZGZsYXJlIOWNjuS4uuS6kSDoh6rlt7HliqjmiYvlrp7njrBMdWEg6ZqP5Lmm5rqQ5Luj56CBcGhw55u05pKtZ2/nm7Tmkq0g55+t6KeG6aKRIOebtOaSreW4pui0pyDku7/mr5Tlv4Mg54yO5ri4IHR06K+t6Z+z6IGK5aSpIOe+juWls+e6pueOqSDpmarnjqnns7vnu5/mupDnoIHlvIDpu5Eg57qm546p5rqQ56CBIOekvuWMuuW8gOa6kCDkupHljp/nlJ/nmoTlpJrkupHlkozmt7flkIjkupHono3lkIjlubPlj7AgSmlhanVu55qE57yW56iL6ZqP5oOzR29sYW5n6K+t6KiA56S+5Yy6IOiFvuiur+ivvuWggiDnvZHmmJPkupHor77loIIg5a2X6IqC5pWZ6IKy6K++56iLUFBU5Y+K5Luj56CB5Z+65LqOR0YgR28gRnJhbWUg55qE5ZCO5Y+w566h55CG57O757uf5bim5L2g5LqG6Kej5LiA5LiLR29sYW5n55qE5biC5Zy66KGM5oOFbXlzcWzooajnu5PmnoToh6rliqjlkIzmraXlt6Xlhbcg55uu5YmN5Y+q5pSv5oyB5a2X5q61IOe0ouW8leeahOWQjOatpSDliIbljLrnrYnpq5jnuqflip/og73mmoLkuI3mlK/mjIEg5Z+65LqOS3ViZXJuZXRlc+eahFBhYVPlubPlj7DmtYHlqpLkvZNOZXRGbGl46Kej6ZSB5qOA5rWL6ISa5pys56iz5a6a5YiG5pSvMiA5IFgg54mI5pys5bey5pu05pawIOeUsSBHb2xhbmfor63oqIDmuLjmiI/mnI3liqHlmagg57u05oqkIOWFqOeQg+acjea4uOaIj+acjeWKoeWZqOWPiuWMuuWfn+acjeahhuaetiDnm67liY3ljY/orq7mlK/mjIF3ZWJzb2NrZXQgS0NQIFRDUOWPilJQQyDph4fnlKjnirbmgIHlkIzmraUg5bin5ZCM5q2l5YaF5rWLIOaEv+aZryDmiZPpgKBNTU/lpJrkurrnq57mioDmuLjmiI/moYbmnrYg5Yqf6IO95oyB57ut5pu05paw5LitIOWfuuS6jiBHb2xhbmcg57G75Ly855+l5LmO55qE56eB5pyJ6YOo572y6Zeu562U5bqU55SoIOWMheWQq+mXruetlCDor4Torrog54K56LWeIOeuoeeQhuWQjuWPsOetieWKn+iDveWFqOaWsOeahOW8gOa6kOa8j+a0nua1i+ivleahhuaetiDlrp7njrBwb2PlnKjnur/nvJbovpEg6L+Q6KGMIOaJuemHj+a1i+ivlSDkvb/nlKjmlofmoaMgWEFQSSBNQU5BR0VSIOS4k+S4muWunueUqOeahOW8gOa6kOaOpeWPo+euoeeQhuW5s+WPsCDkuLrnqIvluo/lvIDlj5HogIXmj5DkvpvkuIDkuKrngbXmtLsg5pa55L6/IOW/q+aNt+eahEFQSeeuoeeQhuW3peWFtyDorqlBUEnnrqHnkIblj5jnmoTmm7TliqDmuIXmmbAg5piO5pyXIOWmguaenOS9oOinieW+l3hBcGnlr7nkvaDmnInnlKjnmoTor50g5Yir5b+Y5LqG57uZ5oiR5Lus54K55Liq6LWe5ZOmIHFx5Y2P6K6u55qEZ29sYW5n5a6e546wIOenu+akjeS6jm1pcmFpZ2/niYjmnKzmnoHnroDlt6XkvZzmtYHlvJXmk47lhajlubPlj7BHb+W8gOa6kOWGhee9kea4l+mAj+aJq+aPj+WZqOahhuaetiBXaW5kb3dzIExpbnV4IE1hY+WGhee9kea4l+mAjyDkvb/nlKjlroPlj6/ovbvmnb7kuIDplK7mibnph4/mjqLmtYtD5q61IELmrrUgQeauteWtmOa0u+S4u+acuiDpq5jljbHmvI/mtJ7mo4DmtYtNUzE3IDEgU21iR2hvc3Qg6L+c56iL5omn6KGMU1NIIFdpbnJtIOWvhueggeeIhuegtOerr+WPo+aJq+aPj+acjeWKoeivhuWIq1BvcnRTY2Fu5oyH57q56K+G5Yir5aSa572R5Y2h5Li75py6IOerr+WPo+aJq+aPj+acjeWKoeivhuWIq1BvcnRTY2FuIGlpa2lyYSBCYWlkdVBDUyBHb+WOn+eJiOWfuuehgOS4iumbhuaIkOS6huWIhuS6q+mTvuaOpSDnp5LkvKDpk77mjqXovazlrZjlip/og70gZeetvuWuneWuieWFqOWboumYn+enr+e0r+WNgeWHoOW5tOeahOWuieWFqOe7j+mqjCDpg73lsIblr7nlpJbpgJDmraXlvIDmlL4g6aaW5byA55qERWhvbmV55qy66aqX6Ziy5b6h57O757ufIOivpeezu+e7n+aYr+WfuuS6juS6keWOn+eUn+eahOasuumql+mYsuW+oeezu+e7nyDkuZ/mmK/kuJrnlYzllK/kuIDlvIDmupDnmoTlr7nmoIfllYbkuJrns7vnu5/nmoTkuqflk4Eg5qy66aqX6Ziy5b6h57O757uf6YCa6L+H6YOo572y6auY5Lqk5LqS6auY5Lu/55yf6Jyc572Q5Y+K5rWB6YeP5Luj55CG6L2s5Y+RIOWGjee7k+WQiOiHqueglOWvhuetvuWPiuivsemltSDlsIbmlLvlh7vogIXmlLvlh7vlvJXlr7zliLDonJznvZDkuK3ovr7liLDmibDkubHlvJXlr7zku6Xlj4rlu7bov5/mlLvlh7vnmoTmlYjmnpwg5Y+v5Lul5b6I5aSn56iL5bqm5LiK5L+d5oqk5Lia5Yqh55qE5a6J5YWoIOaKpOe9keW/heWkh+iJr+iNr+a8guS6rueahEdv6K+t6KiA6YCa55So5ZCO5Y+w566h55CG5qGG5p62IOWMheWQq+iuoeWIkuS7u+WKoSBNeVNRTOeuoeeQhiBSZWRpc+euoeeQhiBGVFDnrqHnkIYgU1NI566h55CGIOacjeWKoeWZqOeuoeeQhiBDYWRkeemFjee9riDkupHlrZjlgqjnrqHnkIbnrYnlip/og70g5b6u5L+h5pSv5LuYIFdlQ2hhdCBQYXkgU0RLIGZvciBHb2xhbmfnlKjkuo7nm5Hmjqfns7vnu5/nmoTml6Xlv5fph4fpm4ZhZ2VudCDlj6/ml6DnvJ3lr7nmjqVvcGVuIGZhbGNvbumYv+mHjOW3tOW3tG15c3Fs5pWw5o2u5bqTYmlubG9n55qE5aKe6YeP6K6i6ZiFIOa2iOi0uee7hOS7tiBDYW5hbCDnmoQgZ28g5a6i5oi356uvIGdpdGh1YiBjb20gYWxpYmFiYSBjYW5hbCDnlKjkuo7mr5TovoMy5LiqcmVkaXPmlbDmja7mmK/lkKbkuIDoh7Qg5pSv5oyB5Y2V6IqC54K5IOS4u+S7jiDpm4bnvqTniYgg5Lul5Y+K5aSa56eNcHJveHkg5pSv5oyB5ZCM5p6E5Lul5Y+K5byC5p6E5a+55q+UIHJlZGlz55qE54mI5pys5pSv5oyBMiB4IDUgeCDkvb/nlKhnbyBtaWNyb+W+ruacjeWKoeWunueOsOeahOWcqOe6v+eUteW9semZouiuouelqOezu+e7n+WQjuerr+S4gOermeW8j+W+ruacjeWKoeahhuaetiDmj5DkvptBUEkgd2ViIHdlYnNvY2tldCBSUEMg5Lu75Yqh6LCD5bqmIOa2iOaBr+a2iOi0ueacjeWKoeWZqOe6ouiTneWvueaKl+i3qOW5s+WPsOi/nOaOp+W3peWFt0ludGVyY2hhaW4gcHJvdG9jb2wg6Leo6ZO+5Y2P6K6u566A5Y2V5piT55SoIOi2s+Wkn+i9u+mHjyDmgKfog73lpb3nmoQgR29sYW5nIOW6k+S4gOS4qmdvIGVjaG8gdnVlIOW8gOWPkeeahOW/q+mAnyDnroDmtIEg576O6KeCIOWJjeWQjuerr+WIhuemu+eahOS4quS6uuWNmuWuouezu+e7nyBibG9nIOS5n+WPr+aWueS+v+S6jOasoeW8gOWPkeS4ukNNUyDlhoXlrrnnrqHnkIbns7vnu58g5ZKM5ZCE56eN5LyB5Lia6Zeo5oi3572R56uZIOato+WcqOabtOaWsOadg+mZkOeuoeeQhiBoYXV0aOmhueebriDkuI3mmK/kuIDkuKrliY3nq69vcuWQjuWPsOahhuaetiDogIzmmK/kuIDkuKrpm4bmiJDmnYPpmZDnrqHnkIYg6I+c5Y2V6LWE5rqQ566h55CGIOWfn+euoeeQhiDop5LoibLnrqHnkIYg55So5oi3566h55CGIOe7hOe7h+aetuaehOeuoeeQhiDmk43kvZzml6Xlv5fnrqHnkIbnrYnnrYnnmoTlv6vpgJ/lvIDlj5HlubPlj7DvvI4gaGF1dGjmmK/kuIDkuKrln7rnoYDkuqflk4Eg5Zyo6L+Z5Liq5Z+656GA5Lqn5ZOB5LiKIOagueaNruS4muWKoemcgOaxgiDlv6vpgJ/nmoTlvIDlj5HlupTnlKjmnI3liqHvvI7otKblj7cgYWRtaW4g5a+G56CBIDEyMzQ1NumAmueUqOeahOaVsOaNrumqjOivgeS4jui/h+a7pOW6kyDkvb/nlKjnroDljZUg5YaF572u5aSn6YOo5YiG5bi455So6aqM6K+BIOi/h+a7pOWZqCDmlK/mjIHoh6rlrprkuYnpqozor4Hlmagg6Ieq5a6a5LmJ5raI5oGvIOWtl+autee/u+ivkSBDVEYgQVdEIEF0dGFjayB3aXRoIERlZmVuc2Ug57q/5LiL6LWb5bmz5Y+wIEFXRCBwbGF0Zm9ybSDmrKLov44gU3RhciDok53psrjmmbrkupHlrrnlmajnrqHnkIblubPlj7AgQmx1ZUtpbmcgQ29udGFpbmVyIFNlcnZpY2Ug56iL5bqP5ZGY5aaC5L2V5LyY6ZuF55qE5oyj6Zu26Iqx6ZKxIDIg54mIIOWNh+e6p+S4uuWwj+S5puS6hiDkuIDkuKogUEhQIOW+ruS/oSBTREtBViDnlLXlvbHnrqHnkIbns7vnu58gYXZtb28gamF2YnVzIGphdmxpYnJhcnkg54is6JmrIOe6v+S4iiBBViDlvbHniYflm77kuabppoYgQVYg56OB5Yqb6ZO+5o6l5pWw5o2u5bqTVGhpbmtQSFAgRnJhbWV3b3JrIOWNgeW5tOWMoOW/g+eahOmrmOaAp+iDvVBIUOahhuaetiDmnIDlhajnmoTliY3nq6/otYTmupDmsYfmgLvku5PlupMg5YyF5ous5YmN56uv5a2m5LmgIOW8gOWPkei1hOa6kCDmsYLogYzpnaLor5XnrYkg5aSa6K+t6KiA5aSa6LSn5biB5aSa5YWl5Y+j55qE5byA5rqQ55S15ZWGIEIyQyDllYbln44g5pSv5oyB56e75Yqo56uvdnVlIGFwcCBodG1sNSDlvq7kv6HlsI/nqIvluo/lvq7lupcg5b6u5L+h5bCP56iL5bqP5ZWG5Z+O562J5Y+v6IO95piv5oiR55So6L+H55qE5pyA5LyY6ZuF55qEIEFsaXBheSDlkowgV2VDaGF0IOeahOaUr+S7mCBTREsg5omp5bGV5YyF5LqGIOWfuuS6juivjeW6k+eahOS4reaWh+i9rOaLvOmfs+S8mOi0qOino+WGs+aWueahiCDmiJHnlKjniKzomavkuIDlpKnml7bpl7TigJzlgbfkuobigJ3nn6XkuY7kuIDnmb7kuIfnlKjmiLcg5Y+q5Li66K+B5piOUEhQ5piv5LiW55WM5LiK5pyA5aW955qE6K+t6KiAIOaJgOS9v+eUqOeahOeoi+W6j+W+ruS/oSBTREsgZm9yIExhcmF2ZWwg5Z+65LqOIG92ZXJ0cnVlIHdlY2hhdOW8gOa6kOWcqOe6v+aVmeiCsueCueaSreezu+e7nyDkuIDmrL7mu6HotrPkvaDnmoTlpJrnp43lj5HpgIHpnIDmsYLnmoTnn63kv6Hlj5HpgIHnu4Tku7Yg5Z+65LqOIExhcmF2ZWwg55qE5ZCO5Y+w57O757uf5p6E5bu65bel5YW3IExhcmF2ZWwgQWRtaW4g5L2/55So5b6I5bCR55qE5Luj56CB5b+r6YCf5p6E5bu65LiA5Liq5Yqf6IO95a6M5ZaE55qE6auY6aKc5YC85ZCO5Y+w57O757ufIOWGhee9ruS4sOWvjOeahOWQjuWPsOW4uOeUqOe7hOS7tiDlvIDnrrHljbPnlKgg6K6p5byA5Y+R6ICF5ZGK5Yir5YaX5p2C55qESFRNTOS7o+eggeS4gOS4quaDs+W4ruS9oOaAu+e7k+aJgOacieexu+Wei+eahOS4iuS8oOa8j+a0nueahOmdtuWcuuS8mOmbheeahOa4kOi/m+W8j1BIUOmHh+mbhuahhuaetiBMYXJhdmVsIOeUteWVhuWunuaImOaVmeeoi+eahOmhueebruS7o+eggVBheW1lbnTmmK9waHDniYjmnKznmoTmlK/ku5jogZrlkIjnrKzkuInmlrlzZGsg6ZuG5oiQ5LqG5b6u5L+h5pSv5LuYIOaUr+S7mOWuneaUr+S7mCDmi5vllYbkuIDnvZHpgJrmlK/ku5gg5o+Q5L6b57uf5LiA55qE6LCD55So5o6l5Y+jIOaWueS+v+W/q+mAn+aOpeWFpeWQhOenjeaUr+S7mCDmn6Xor6Ig6YCA5qy+IOi9rOi0puiDveWKmyDmnI3liqHnq6/mjqXlhaXmlK/ku5jlip/og70g5pa55L6/IOW/q+aNtyBTUEYgU3dvb2xlIFBIUCBGcmFtZXdvcmsg5LiW55WM56ys5LiA5qy+5Z+65LqOU3dvb2xl5omp5bGV55qEUEhQ5qGG5p62IOW8gOWPkeiAheaYr1N3b29sZeWIm+Wni+S6uiBBIFdvbmRlcmZ1bCBXb3JkUHJlc3MgVGhlbWUg5qix6Iqx5bqE55qE55m954yr5Y2a5a6i5Li76aKY5Zu+5bqKIOatpOmhueebruW3suW8g+eUqCDln7rkuo4gVGhpbmtQSFAg5Z+656GA5byA5Y+R5bmz5Y+wIOeZu+W9lei0puWPt+WvhueggemDveaYryBhZG1pbiBQYW5Eb3dubG9hZOe9kemhteWkjeWIu+eJiOS4gOS4quW8gOa6kOeahOe9keWdgOWvvOiIque9keermemhueebriDmgqjlj6/ku6Xmi7/mnaXliLbkvZzoh6rlt7HnmoTnvZHlnYDlr7zoiKog5L2/55SoUEhQIFN3b29sZeWunueOsOeahOe9kemhteWNs+aXtuiBiuWkqeW3peWFtyDni6zop5LmlbDljaEg5Y+R5Y2hIOW8gOa6kOW8j+ermemVv+iHquWKqOWMluWUrui0p+ino+WGs+aWueahiCDpq5jmlYgg56iz5a6aIOW/q+mAnyDljaHlr4bllYbln47ns7vnu58g6auY5pWI5a6J5YWo55qE5Zyo57q/5Y2h5a+G5ZWG5Z+OIO+4j+WRveS7pOihjOaooeW8j+W8gOWPkeahhuaetlNob3BYT+WFjei0ueW8gOa6kOWVhuWfjuezu+e7nyDlm73lhoXpooblhYjkvIHkuJrnuqdCMkPlhY3otLnlvIDmupDnlLXllYbns7vnu58g5YyF5ZCrUEMgaDUg5b6u5L+h5bCP56iL5bqPIOaUr+S7mOWuneWwj+eoi+W6jyDnmb7luqblsI/nqIvluo8g5aS05p2hIOaKlumfs+Wwj+eoi+W6jyBRUeWwj+eoi+W6jyBBUFAg5aSa5ZWG5oi3IOmBteW+qk1JVOW8gOa6kOWNj+iuruWPkeW4gyDln7rkuo4gVGhpbmtQSFA1IDHmoYbmnrbnoJTlj5FXaXphcmTmmK/kuIDmrL7lvIDmupDnmoTmlofmoaPnrqHnkIblt6Xlhbcg5pSv5oyBTWFya2Rvd24gU3dhZ2dlciBUYWJsZeexu+Wei+eahOaWh+ahoyBTd29vbGUgTXlTUUwgUHJveHkg5LiA5Liq5Z+65LqOIE15U1FMIOWNj+iuriBTd29vbGUg5byA5Y+R55qETXlTUUzmlbDmja7lupPov57mjqXmsaAg5a2m5Lmg6LWE5rqQ5pW05ZCIRnJlZW5vbeWfn+WQjeiHquWKqOe7reacn+S4gOS4quWlveeOqeeahFdlYuWuieWFqCDmvI/mtJ7mtYvor5XlubPlj7DkuIDkuKrln7rkuo5ZaWky6auY57qn5qGG5p6255qE5b+r6YCf5byA5Y+R5bqU55So5byV5pOO6JOd5aSp6YeH6ZuG5Zmo5piv5LiA5qy+5YWN6LS555qE5pWw5o2u6YeH6ZuG5Y+R5biD54is6Jmr6L2v5Lu2IOmHh+eUqHBocCBteXNxbOW8gOWPkSDlj6/pg6jnvbLlnKjkupHmnI3liqHlmagg5Yeg5LmO6IO96YeH6ZuG5omA5pyJ57G75Z6L55qE572R6aG1IOaXoOe8neWvueaOpeWQhOexu0NNU+W7uuermeeoi+W6jyDlhY3nmbvlvZXlrp7ml7blj5HluIPmlbDmja4g5YWo6Ieq5Yqo5peg6ZyA5Lq65bel5bmy6aKEIOaYr+e9kemhteWkp+aVsOaNrumHh+mbhui9r+S7tuS4reWujOWFqOi3qOW5s+WPsOeahOS6keerr+eIrOiZq+ezu+e7n+WFjei0ueW8gOa6kOeahOS4reaWh+aQnOe0ouW8leaTjiDph4fnlKggQyBDIOe8luWGmSDln7rkuo4geGFwaWFuIOWSjCBzY3dzIOaPkOS+myBQSFAg55qE5byA5Y+R5o6l5Y+j5ZKM5Liw5a+M5paH5qGjV0RTY2FubmVy5bmz5Y+w55uu5YmN5a6e546w5LqG5aaC5LiL5Yqf6IO9IOWIhuW4g+W8j3dlYua8j+a0nuaJq+aPjyDlrqLmiLfnrqHnkIYg5ryP5rSe5a6a5pyf5omr5o+PIOWtkOWfn+WQjeaemuS4viDnq6/lj6Pmiavmj48g572R56uZ54is6JmrIOaal+mTvuajgOa1iyDlnY/pk77mo4DmtYsg572R56uZ5oyH57q55pCc6ZuGIOS4k+mhuea8j+a0nuajgOa1iyDku6PnkIbmkJzpm4blj4rpg6jnvbLnrYnlip/og70g77iP5YWw56m65Zu+5bqK5Zu+5qCH5bel5Zy6IOenu+WKqOW6lOeUqOWbvuagh+eUn+aIkOW3peWFtyDkuIDplK7nlJ/miJDmiYDmnInlsLrlr7jnmoTlupTnlKjlm77moIflkozlkK/liqjlm74gQXJnb24g5LiA5Liq6L2755uIIOeugOa0geeahCBXb3JkUHJlc3Mg5Li76aKYVHlwZWNobyBGYW5z5o+S5Lu25L2c5ZOB55uu5b2VUEhQ5Luj56CB5a6h6K6h5YiG5q616K6y6Kej5LiA5Liq57uT5p6E5riF5pmw55qEIOaYk+S6jue7tOaKpOeahCDnjrDku6PnmoRQSFAgTWFya2Rvd27op6PmnpDlmajnmb7luqbotLTlkKfkupHnrb7liLAg5Zyo5pyN5Yqh5Zmo5LiK6YWN572u5aW95bCx5peg6ZyA6L+b6KGM5Lu75L2V5pON5L2c5L6/5Y+v5Lul5a6e546w6LS05ZCn55qE5YWo6Ieq5Yqo562+5YiwIOmFjeWQiOaPkuS7tuS9v+eUqOi/mOWPr+WunueOsOS6keeBjOawtCDngrnotZ4g5bCB56aBIOWIoOW4liDlrqHmn6XnrYnlip/og70g5rOo5oSPIEdpdGVlIOWOn0dpdCBvc2Mg5LuT5bqT5bCG5LiN5YaN57u05oqkIOebruWJjeWUr+S4gOaMh+WumueahOS7k+W6k+S4uiBHaXRodWIg5pys6aG555uu5rKh5pyJ5a6Y5pa55Lqk5rWB576kIOWmgumcgOS6pOa1geWPr+S7peebtOaOpeS9v+eUqEdpdGh1YueahERpc2N1c3Npb25zIOayoeacieWVhuS4mueJiOacrCDnm67liY3otLTlkKfkupHnrb7liLDnlLHnpL7ljLrlhbHlkIznu7TmiqQg5LiN5Lya5YGc5q2i5pu05pawIFBSIOmAmuW4uOWcqOS4gOWkqeWGheWkhOeQhiDlvq7kv6HosIPor5UgQVBJ6LCD6K+V5ZKMQUpBWOeahOiwg+ivleeahOW3peWFtyDog73lsIbml6Xlv5fpgJrov4dXZWJTb2NrZXTovpPlh7rliLBDaHJvbWXmtY/op4jlmajnmoRjb25zb2xl5LitIOe1kOW3tCDkuK3mlofliIboqZ4g5YGa5pyA5aW955qEIFBIUCDkuK3mlofliIboqZ4g5Lit5paH5pa36Kme57WE5Lu2RWxlVGVhbeW8gOa6kOmhueebriDnlLXllYblhajlpZfop6PlhrPmlrnmoYjkuYtQSFDniYggU2hvcCBmb3IgUEhQIFlpaTIg5LiA5Liq57G75Ly85Lqs5LicIOWkqeeMqyDmt5jlrp3nmoTllYbln44g5pyJ5a+55bqU55qEQVBQ5pSv5oyBIOeUsUVsZVRlYW3lm6LpmJ/nu7TmiqQgUmhhUEhQ5piv5b6u5L+h56ys5LiJ5pa5566h55CG5bmz5Y+wIOW+ruS/oeWFrOS8l+WPt+euoeeQhuezu+e7nyDmlK/mjIHlpJrlhazkvJflj7fnrqHnkIYgQ1JN5Lya5ZGY566h55CGIOWwj+eoi+W6j+W8gOWPkSBBUFDmjqXlj6PlvIDlj5Eg5Yeg5LmO6ZuG5ZCI5b6u5L+h5Yqf6IO9IOeugOa0gSDlv6vpgJ/kuIrmiYsg5b+r6YCf5byA5Y+R5b6u5L+h5ZCE56eN5ZCE5qC35bqU55SoIOeugOa0gSDlpb3nlKgg5b+r6YCfIOmhueebruW8gOWPkeW/q+WHoOWAjSDnvqQgNjU2ODY4IOS4gOWIu+ekvuWMuuWQjuerryBBUEkg5rqQ56CBIOaWsCDlvq7kv6HmnI3liqHlj7cg5b6u5L+h5bCP56iL5bqPIOW+ruS/oeaUr+S7mCDmlK/ku5jlrp3mlK/ku5joi7nmnpxjbXMgdjEgbWFjY21zIHYxIOm6puWFi2NtcyDlvIDmupBjbXMg5YaF5a65566h55CG57O757ufIOinhumikeWIhuS6q+eoi+W6jyDliIbpm4bliafmg4XnqIvluo8g572R5Z2A5a+86Iiq56iL5bqPIOaWh+eroOeoi+W6jyDmvKvnlLvnqIvluo8g5Zu+54mH56iL5bqP5LiA5LiqUEhQ5paH5Lu25pCe5a6a5pSv5LuY5a6d5pSv5LuY57O75YiXIOWMheaLrOeUteiEkee9keermeaUr+S7mCDmiYvmnLrnvZHnq5nmlK/ku5gg546w6YeR57qi5YyFIOa2iOi0uee6ouWMhSDmiavnoIHmlK/ku5ggSlNBUEnmlK/ku5gg5Y2V56yU6L2s6LSm5Yiw5pSv5LuY5a6d6LSm5oi3IOS6pOaYk+e7k+eulyDliIbotKYg5YiG5ramIOe9kemhteaOiOadg+iOt+WPlueUqOaIt+S/oeaBr+etiXJlc3RmdWwgYXBp6aOO5qC85o6l5Y+jIEFQUOaOpeWPoyBBUFDmjqXlj6PmnYPpmZAgb2F1dGgyIOaOpeWPo+eJiOacrOeuoeeQhiDmjqXlj6PpibTmnYPln7rkuo7kvIHkuJrlvq7kv6HnmoTlvIDmupBTQ1JN5bqU55So5byA5Y+R5qGG5p62IOW8leaTjiDkuZ/mmK/kuIDlpZfpgJrnlKjnmoTkvIHkuJrnp4Hln5/mtYHph4/nrqHnkIbns7vnu58gQVBJ5o6l5Y+j5aSn5YWo5LiN5pat5pu05paw5LitIOasoui/jkZvcmvlkoxTdGFyIDEg5LiA6KiAIOWPpOivl+WPpeeJiCBhcGkgMiDlv4XlupTmr4/ml6XkuIDlm75hcGkgMyDlnKjnur9pcOafpeivoiA0IG0zdTjop4bpopHlnKjnur/op6PmnpBhcGkgNSDpmo/mnLrnlJ/miJDkuozmrKHlhYPlm77niYdhcGkgNiDlv6vpgJLmn6Xor6JhcGkg5pSv5oyB5Zu95YaF55m+5a625b+r6YCSIDcgZmx26KeG6aKR5Zyo57q/6Kej5p6QYXBpIDgg5oqW6Z+z6KeG6aKR5peg5rC05Y2w6Kej5p6QYXBpIDkg5LiA5Y+l6K+d6ZqP5py65Zu+54mHYXBpIDEgUVHnlKjmiLfkv6Hmga/ojrflj5ZhcGkgMTEg5ZOU5ZOp5ZOU5ZOp5bCB6Z2i5Zu+6I635Y+WYXBpIDEyIOWNg+Wbvue9kTU4cGlj5peg5rC05Y2w6Kej5p6Q5LiL6L29YXBpIDEzIOWWnOmprOaLiembheS4u+aSrUZN5pWw5o2u6YeH6ZuGYXBpIDE0IOe9keaYk+S6kemfs+S5kGFwaSAxNSBDQ1RW5aSu6KeG572R6KeG6aKR6Kej5p6QYXBpIDE2IOW+ruS/oei/kOWKqOWIt+atpeaVsGFwaSAxNyDnmq7nmq7mkJ7nrJEg5Z+65LqOc3dvb2xl55qE5a6a5pe25Zmo56iL5bqPIOaUr+aMgeenkue6p+WkhOeQhue+pCA2NTY4Njgg77iPIFNhYmVyIFBIUOW8guatpeWNj+eoi0hUVFDlrqLmiLfnq6/lvq7kv6HmlK/ku5jljZXmlofku7bniYgg5LiA5LiqUEhQ5paH5Lu25pCe5a6a5b6u5L+h5pSv5LuY57O75YiXIOWMheaLrOWOn+eUn+aUr+S7mCDmiavnoIHmlK/ku5ggSDXmlK/ku5gg5YWs5LyX5Y+35pSv5LuYIOeOsOmHkee6ouWMhSDkvIHkuJrku5jmrL7liLDpm7bpkrHnrYkg5paw5aKeVjPniYgg5LiA5Liq6L+Y5LiN6ZSZ55qE5Zu+5bqK5bel5YW3IOaUr+aMgU1hYyBXaW4gTGludXjmnI3liqHlmagg5pSv5oyB5Y6L57yp5ZCO5LiK5LygIOa3u+WKoOWbvueJh+aIluaWh+Wtl+awtOWNsCDlpJrmlofku7blkIzml7bkuIrkvKAg5ZCM5pe25LiK5Lyg5Yiw5aSa5Liq5LqRIOWPs+WHu+S7u+aEj+aWh+S7tuS4iuS8oCDlv6vmjbfplK7kuIrkvKDliarotLTmnb/miKrlm74gV2Vi54mI5LiK5LygIOaUr+aMgeS9nOS4uk13ZWIgVHlwb3Jh5Y+R5biD5Zu+54mH5o6l5Y+jIOS9nOS4ulBpY0dvIFNoYXJlWCB1UGlj562J55qE6Ieq5a6a5LmJ5Zu+5bqKIOaUr+aMgeWcqOacjeWKoeWZqOS4iumDqOe9suS9nOS4uuWbvuW6iuaOpeWPoyDmlK/mjIHkuIrkvKDku7vmhI/moLzlvI/mlofku7Yg5Y+v6IO95piv5oiR55So6L+H55qE5pyA5LyY6ZuF55qEIEFsaXBheSDlkowgV2VDaGF0IOeahCBsYXJhdmVsIOaUr+S7mOaJqeWxleWMheS6huS4iuS8oOWkp+aWh+S7tueahExhcmF2ZWzmianlsZXljIXlvIDlj5HlhoXlip/kv67ngrxMYXJhdmVs5qC45b+D5Luj56CB5a2m5Lmg5Y2X5Lqs6YKu55S15aSn5a2m5byA5rqQIE9ubGluZSBKdWRnZSBRUee+pCA2NjgxIDgyNjQg5YWN6LS5SVDlnLDlnYDmlbDmja7lupMg5bey5pSv5oyBSVBWNCBJUFY2IOe7k+aehOWMlui+k+WHuuS4uuWbveWutiDnnIEg5biCIOWOvyDov5DokKXllYYg5Lit5paH5pWw5o2u5bqTIOaWueS+v+WunueUqCBsYXJhdmVsNSA15ZKMdnVlIGpz57uT5ZCI55qE5YmN5ZCO56uv5YiG56a76aG555uu5qih5p2/IOWQjuerr+S9v+eUqOS6hmxhcmF2ZWznmoRMVFPniYjmnKwgNSA1IOWJjeerr+S9v+eUqOS6hua1geihjOeahHZ1ZSBlbGVtZW50IHRlbXBsYXRl6aG555uuIOS9nOS4uueoi+W6j+eahOi1t+eCuSDlj6/ku6Xnm7TmjqXku6XmraTkuLrln7rnoYDmnaXov5vooYzkuJrliqHmianlsZUg5qih5p2/5YaF5a655YyF5ous5Z+656GA55qE55So5oi3566h55CG5ZKM5p2D6ZmQ566h55CGIOaXpeW/l+euoeeQhiDpm4bmiJDnrKzkuInmlrnnmbvlvZUg5pW05ZCIbGFyYXZlbCBlY2hvIHNlcnZlciDlrp7njrDkuoZ3ZWJzb2NrZXQg5YGa5Yiw5LqG5raI5oGv55qE5a6e5pe25o6o6YCBIOW5tuWcqOatpOWfuuehgOS4iiDlrp7njrDkuobogYrlpKnlrqTlkozlrqLmnI3lip/og70g5p2D6ZmQ566h55CG5YyF5ous5ZCO56uvVG9rZW7orqTor4HlkozliY3nq692dWUganPnmoTliqjmgIHmnYPpmZAg6Kej5Yaz5LqG5YmN5ZCO56uv5a6M5pW05YiG56a755qE5oOF5Ya15LiLIHZ1ZSBqc+eahOiupOivgeS4juadg+mZkOebuOWFs+eahOeXm+eCuSDlt7LlnKjmnKzkurrnmoTlpJrkuKrpobnnm67kuK3pm4bmiJDkvb/nlKggV2Vi5a6J5YWo5LmL5py65Zmo5a2m5Lmg5YWl6ZeoIOe9keaYk+S6kemfs+S5kOWNh+e6p0FQSVBIUCDpm4bmiJDmlK/ku5ggU0RLIOmbhuaIkOS6huaUr+S7mOWunSDlvq7kv6HmlK/ku5jnmoTmlK/ku5jmjqXlj6PlkozlhbblroPnm7jlhbPmjqXlj6PnmoTmk43kvZwg5pSv5oyBIHBocCBmcG0g5ZKMIFN3b29sZSDmiYDmnInmoYbmnrbpgJrnlKgg5a6H5ramUEhQ5YWo5a625qG25oqA5pyv5pSv5oyB576kIDE3OTE2MjI3TURDbHViIOekvuWMuuezu+e7n+WQjuerr+S7o+eggWltaSDmmK/ln7rkuo4gU3dvb2xlIOeahCBQSFAg5Y2P56iL5byA5Y+R5qGG5p62IOWug+aUr+aMgSBIdHRwMiBXZWJTb2NrZXQgVENQIFVEUCBNUVRUIOetieS4u+a1geWNj+iurueahOacjeWKoeW8gOWPkSDnibnliKvpgILlkIjkupLogZTnvZHlvq7mnI3liqEg5Y2z5pe26YCa6K6v6IGK5aSpaW0g54mp6IGU572R562J5Zy65pmvIFFR576kIDE3OTE2MjI3V29yZFByZXNzIOeJiCBXZWJTdGFjayDlr7zoiKrkuLvpopggbmF2IGlvd2VuIGNuTGl2ZTJEIOeci+adv+WomOaPkuS7tiB3d3cgZmdocnNoIG5ldCBwb3N0IDEyMyBodG1sIOS4iuS9v+eUqOeahOWQjuerryBBUEnnroDljZXmkJzntKIg5LiA5Liq566A5Y2V55qE5YmN56uv55WM6Z2iIOeUqOaDr+S6huWQhOenjeWvvOiIqummlumhtSDmu6HlsY/luZXlsL3mmK/lkITnp43kuI3ljozlhbbng6bnmoTlub/lkYrlkozotYTorq8g5bCd6K+V6Ieq5bex5YaZ5Liq6Ieq5bex55qE5Li76aG1IOWbveWGheWQhOWkp0NURui1m+mimOWPindyaXRldXDmlbTnkIbmlLbpm4boh6rnvZHnu5zlkITlpITnmoQgd2Vic2hlbGwg5qC35pysIOeUqOS6jua1i+ivlSB3ZWJzaGVsbCDmiavmj4/lmajmo4DmtYvnjocgUEhQ5b6u5L+hU0RLIOW+ruS/oeW5s+WPsCDlvq7kv6HmlK/ku5gg56CB5bCP5YWtIEdpdEh1YiDku6PnoIHms4TpnLLnm5Hmjqfns7vnu59QSFDooajljZXnlJ/miJDlmagg5b+r6YCf55Sf5oiQ546w5Luj5YyW55qEZm9ybeihqOWNlSDmlK/mjIHliY3lkI7nq6/liIbnprsg5YaF572u5aSN6YCJ5qGGIOWNlemAieahhiDovpPlhaXmoYYg5LiL5ouJ6YCJ5oup5qGGIOecgeW4guWMuuS4iee6p+iBlOWKqCDml7bpl7TpgInmi6kg5pel5pyf6YCJ5oupIOminOiJsumAieaLqSDmlofku7Yg5Zu+54mH5LiK5Lyg562JMTfnp43luLjnlKjnu4Tku7Yg5oKf56m6Q1JNIOWfuuS6jlRQNSB2dWUgRWxlbWVudFVJ55qE5YmN5ZCO56uv5YiG56a7Q1JN57O757ufVuWFjeetvlBIUOeJiCDlrozlhajlvIDmupDlhY3otLnnmoTkuKrkurrlhY3nrb7nuqbop6PlhrPmlrnmoYhDb21wb3NlciDlhajph4/plZzlg4/lj5HluIPkuo4yIDE35bm0M+aciCDmm77kuI3pl7Tmlq3ov5DooYwy5bm05aSaIOi/meS4quW8gOa6kOacieWKqeS6jueQhuinoyBDb21wb3NlciDplZzlg4/nmoTlt6XkvZzljp/nkIbkuIDkuKrlpJrlvakg6L275p2+5LiK5omLIOS9k+mqjOWujOWWhCDlhbfmnInlvLrlpKfoh6rlrprkuYnlip/og73nmoRXb3JkUHJlc3PkuLvpopgg5Z+65LqOU2FrdXJh5Li76aKY5YWo55CD5YWN6LS55Luj55CGSVDlupMg6auY5Y+v55SoSVAg57K+5b+D562b6YCJ5LyY6LSoSVAgMnPlv4Xovr5MYXJhQ01TIOaYr+WcqOWtpuS5oCBsYXJhdmVsIHdlYiDlvIDlj5Hlrp7miJjov5vpmLYg5a6e5oiY5p6E5p62IEFQSSDmnI3liqHlmagg6L+H56iL5Lit5Lqn55Sf55qE5LiA5Liq5Lia5L2Z5L2c5ZOBIOivleWbvumAmui/h+eugOWNleeahOaWueW8jyDlv6vpgJ/mnoTlu7rkuIDlpZfln7rmnKznmoTkvIHkuJrnq5nlkIzml7bkv53nlZnlvojngbXmtLvnmoTmianlsZXog73lipvlkozkvJjpm4XnmoTku6PnoIHmlrnlvI8g5b2T54S26L+Z5Lqb6YO95b6X55uKTGFyYXZlbOeahOS8mOengOiuvuiuoSDlkIzml7ZMYXJhQ01TIOS5n+aYr+S4gOS4quWtpuS5oExhcmF2ZWwg5LiN6ZSZ55qE5Y+C6ICD56S65L6LIOW3suWBnOatoue7tOaKpCBIb29rUEhQ5Z+65LqOQ+aJqeWxleaQreW7uuWGhee9rkFJ57yW56iL55qE5p625p6E57O757ufIOaUr+aMgeW+ruacjeWKoemDqOe9siDng63mj5Lmi5TkuJrliqHnu4Tku7Yg6ZuG5oiQ5Lia5Yqh5qih5Z6LIOadg+mZkOaooeWeiyBVSee7hOS7tuW6kyDlpJrmqKHmnb8g5aSa5bmz5Y+wIOWkmuWfn+WQjSDlpJrnu4jnq68g5aSa6K+t6KiAIOWQq+W4uOmpu+WGheWtmCDliY3lkI7liIbnprsgQVBJ5bmz5Y+wIExVQSBRUee+pCA2NzkxMTYzOCDkuK3ljY7kurrmsJHlhbHlkozlm73lsYXmsJHouqvku73or4Eg5Lit5Y2O5Lq65rCR5YWx5ZKM5Zu95riv5r6z5bGF5rCR5bGF5L2P6K+B5Lul5Y+K5Lit5Y2O5Lq65rCR5YWx5ZKM5Zu95Y+w5rm+5bGF5rCR5bGF5L2P6K+B5Y+356CB6aqM6K+B5bel5YW3IFBIUCDniYgg5pyA566A5Y2V55qEOTFwb3Ju54is6JmrcGhw54mI5pysRmVuZCDmmK/kuIDmrL7nn63lsI/nsr7mgo0g5Y+v5ZyoIEZQTSBTd29vbGUg5pyN5Yqh5a655Zmo5bmz5ruR5YiH5o2i55qE6auY5oCn6IO9UEhQ5qGG5p62IG5vIGV2aWwg5a6e546w6L+H5ruk5pWP5oSf6K+N5rGHIOWfuuS6juehruWumuacieept+iHquWKqOacuiBERkEg566X5rOVIOaUr+aMgWNvbXBvc2Vy5a6J6KOF5omp5bGVWiBCbG9nUEhQ5Y2a5a6i56iL5bqPSVlVVeiHquWKqOi+heenjeW3peWFtyDnm67liY3og73lr7nlm73lhoXlpKfpg6jliIbnmoRQVOermeeCueiHquWKqOi+heenjSDmlK/mjIHkuIvovb3lmajpm4bnvqQg5pSv5oyB5aSa55uY5L2NIOaUr+aMgeWkmuS4i+i9veebruW9lSDmlK/mjIHov5znqIvov57mjqXnrYkg5p6c6YWx5bCP5bqXIOWfuuS6jiBMYXJhdmVsIHN3b29sZSDlsI/nqIvluo/nmoTlvIDmupDnlLXllYbns7vnu58g5LyY6ZuF5LiO5oCn6IO95YW86aG+IOmAmeaYr+S4gOS7vee0lOmdoOWMl+W3peeoi+W4q+eahOWwiOahiCDoq4vlpb3lpb3mhJvorbflroMg6Kyd6KydIEVDIGVjamlhIOWIsOWutuaYr+S4gOasvuWPr+W8gOWxlU8yT+S4muWKoeeahOenu+WKqOeUteWVhuezu+e7nyDlroPljIXlkKsg56e75Yqo56uvQVBQIOmHh+eUqOWOn+eUn+aooeW8j+W8gOWPkSDopobnm5bkvb/nlKhpT1Mg5Y+KQW5kcm9pZOezu+e7n+eahOenuyDliqjnu4jnq68g5ZCO5Y+w57O757ufIOmSiOWvueW5s+WPsOaXpeW4uOi/kOiQpee7tOaKpOeahOW5s+WPsOWQjuWPsCDpkojlr7nlhaXpqbvlupfpk7rnrqHnkIbnmoTllYblrrblkI7lj7Ag54us56uL5bm26KGMIOenu+WKqOerr0g1IOiDveWkn+eBtea0u+mDqOe9suS6juW+ruS/oeWPiuWFtuS7lkFQUCDnvZHpobXnrYkgTWF0ZXJpYWwgRGVzaWduIOaMh+WNl+eahOS4reaWh+e/u+ivkSDkuIDkuKrnuq9waHDliIbor40gdGhpbmtwaHA1IDEgbGF5dWkg5a6e546w55qE5bimcmJhY+eahOWfuuehgOeuoeeQhuWQjuWPsCDmlrnkvr/lv6vpgJ/lvIDlj5Hms5Xkvb/nlKjnmb7luqZwY3PkuIrkvKDohJrmnKznm67liY3mnIDlhajnmoTliY3nq6/lvIDlj5HpnaLor5Xpopjlj4rnrZTmoYjmqLHoirHlhoXnvZHnqb/pgI/nvZHnq5nmupDku6PnoIEgMiAyIOmHjeWItueJiE1lZXBvUFPmmK9NZWVwbyBQSFAgU29ja2V055qE57yp5YaZIOaXqOWcqOaPkOS+m+eos+WumueahFNvY2tldOacjeWKoSDlj6/ku6Xovbvmnb7mnoTlu7rlnKjnur/lrp7ml7bogYrlpKkg5Y2z5pe25ri45oiPIOinhumikea1geWqkuS9k+aSreaUvuetiSDln7rnoYDnm67lvZUg6IGa5ZCI5omA5pyJ5YW25LuW55uu5b2VIOWMheWQq+aWh+aho+WSjOS+i+WtkOWfuuS6jiBWdWUganMg55qE566A5rSB5LiA6Iis5by65aSn55qEIFdvcmRQcmVzcyDljZXmoI/ljZrlrqLkuLvpopjpmL/ph4zkupHmiZPpgKBMYXJhdmVs5pyA5aW955qET1NTIFN0b3JhZ2XmianlsZUg572R5LiK5Zyo57q/5ZWG5Z+OIOe7vOWQiOe9keS4iui0reeJqeW5s+WPsHN3b29sZWZ55piv5LiA5Liq5Z+65LqOc3dvb2xl5a6e546w55qE6L276YeP57qnIOmrmOaAp+iDvSDljY/nqIvnuqcg5byA5pS+5oCn55qEQVBJ5bqU55So5pyN5Yqh5qGG5p625Z+65LqOcmVkaXPlrp7njrDpq5jlj6/nlKgg5piT5ouT5bGVIOaOpeWFpeaWueS+vyDnlJ/kuqfnjq/looPnqLPlrprov5DooYznmoTlu7bov5/pmJ/liJcg5LiA5qy+5Z+65LqOV29yZFByZXNz5byA5Y+R55qE6auY6aKc5YC855qE6Ieq6YCC5bqU5Li76aKYIOaUr+aMgeeZveWkqeS4jum7keWknOaooeW8jyDml6DliLfmlrDliqDovb3nrYkg6Zi/6YeM5LqRIE9TUyDlrpjmlrkgU0RLIOeahCBDb21wb3NlciDlsIHoo4Ug5pSv5oyB5Lu75L2VIFBIUCDpobnnm64g5YyF5ousIExhcmF2ZWwgU3ltZm9ueSBUaW55TGFyYSDnrYnnrYkg5q2k5o+S5Lu25bCG5L2g55qEV29yZFByZXNz5o6l5YWl5pys5Zyf55Sf5oCB5L2T57O75LmL5LitIOS9v+S5i+abtOmAguWQiOWbveWGheW6lOeUqOeOr+Wig1BIUOeahOacjeWKoeWMluahhuaetiDpgILnlKjkuo5BcGkgU2VydmVyIFJwYyBTZXJ2ZXIg5biu5Yqp5Y6f55SfUEhQ6aG555uu6L2s5ZCR5b6u5pyN5Yqh5YyWIOWHuuiJsueahOaAp+iDveS4juaUr+aMgemrmOW5tuWPkeeahOWNj+eoi+ebuOe7k+WQiOWfuuS6jlRoaW5rUEhQIFY2IOW8gOWPkeeahOmdouWQkUFQSeeahOWQjuWPsOeuoeeQhuezu+e7nyBQSFAgU3dvb2xlIOW8gOWPkeeahOWcqOe6v+WQjOatpeeCueatjOWPsCDmlK/mjIHoh6rnlLHngrnmrYwg5YiH5q2MIOiwg+aVtOaOkuW6jyDliKDpmaTmjIflrprpn7PkuZDku6Xlj4rln7rnoYDmnYPpmZDliIbnuqfkv6Hlkbwg5YWN6LS55byA5rqQ55qE5Yqe5YWsT0Hns7vnu58g5YyF5ousQVBQIHBj5LiK5a6i5oi356uvIFJFSU3ljbPml7bpgJrkv6Eg5pyN5Yqh56uv562JIOiuqeavj+S4quS8geS4muWNleS9jemDveacieiHquW3seeahOWKnuWFrOezu+e7nyDmnaXlrqLnlLXllYYg5b6u5L+h5bCP56iL5bqP5ZWG5Z+OIEFQUOWVhuWfjiDlhazkvJflj7fllYbln44gUEPllYbln47ns7vnu58g5pSv5LuY5a6d5bCP56iL5bqP5ZWG5Z+OIOaKlumfs+Wwj+eoi+W6j+WVhuWfjiDnmb7luqblsI/nqIvluo/nlLXllYbns7vnu58g5YmN5ZCO56uv5Luj56CB5YWo6YOo5byA5rqQIOazqOmHjeeVjOmdoue+juaEn+S4jueUqOaIt+S9k+mqjCDmiZPpgKDni6znibnnlLXllYbns7vnu5/nlJ/mgIHlnIjlk5Tlk6nlk5Tlk6kgQmlsaWJpbGkgQiDnq5nkuLvnq5nliqnmiYsg55u05pKt5Yqp5omLIOebtOaSreaKveWlliDmjILmnLrljYfnuqcg6LS05b+D5bCP5qOJ6KKE6ISa5pysIEx2NiDnprvkvaDku4XmnInkuIDmraXkuYvpgaUgUEhQIOeJiCBQZXJzb25hbCDkuIDkuKrov5DnlKhwaHDkuI5zd29vbGXlrp7njrDnmoTnu5/orqHnm5Hmjqfns7vnu5/nn63op4bpopHljrvmsLTljbAg5oqW6Z+zIOearuearuiZviDngavlsbEg5b6u6KeGIOW+ruWNmiDnu7/mtLIg5pyA5Y+zIOi9u+inhumikSDlv6vmiYsg5YWo5rCR5bCP6KeG6aKRIOW3tOWhnueUteW9sSDpmYzpmYwgQmVmb3Jl6YG/6aOOIOW8gOecvCBWdWUgVmxvZyDlsI/lkpbnp4Ag55qu55qu5pCe56yRIOWFqOawkUvmrYwg6KW/55Oc6KeG6aKRIOS4reWbveWGnOWOhiDpmLTljoYg5LiO6Ziz5Y6GIOWFrOWOhiDovazmjaLkuI7mn6Xor6Llt6XlhbdBb2lBV0Qg5LiT5Li65q+U6LWb6K6+6K6hIOS+v+aQuuaAp+WlvSDkvY7mnYPpmZDov5DooYznmoRFRFLns7vnu58g6aG555uu566h55CG57O757uf5ZCO56uv5o6l5Y+jVGhpbmtQSFAg6Zif5YiX5pSv5oyBVHlwZWNobyBUaGVtZSBBcmlhIOS5puWGmeiHquW3seeahOevh+eroFBIUCDkuK3mloflt6XlhbfljIUg5pSv5oyB5rGJ5a2X6L2s5ou86Z+zIOaLvOmfs+WIhuivjSDnroDnuYHkupLovawg5pWw5a2XIOmHkemineWkp+WGmSBRUee+pCAxNzkxNjIyN1lpaTIgY29tbXVuaXR5IOivt+iuv+mXrua3mOWuojXlkIjkuIBTREsg5pSv5oyB5reY5a6d6IGU55ufIOS6rOS4nOiBlOebnyDlpJrlpJrov5vlrp0g5ZSv5ZOB5LyaIOiLj+WugeWfuuS6jiB0aGlua3BocCDlvIDlj5HnmoTnmoQgYmxvZ01vaml0byBBZG1pbiDln7rkuo4gTGFyYXZlbCBWdWUgRWxlbWVudCDmnoTlu7rnmoTlkI7lj7DnrqHnkIbns7vnu5/kuIDkuKrnu4/lhbjnmoRYU1PmuJfpgI/nrqHnkIblubPlj7DkuIDmrL7ln7rkuo4gUmFnZUZyYW1lMiDnmoTlhY3otLnlvIDmupDnmoTln7rnoYDplIDllK7lip/og73nmoTllYbln47ln7rkuo5MYXJhdmVsIDUgNCDnmoTlvIDlj5HnmoTljZrlrqLns7vnu58g5Luj5Y+3IG15UGVyc2ltbW9u6K+B5Lu254Wn54mH5o6S54mI5Zyo57q/55Sf5oiQ5ZmoIOWcqOS4gOW8oDblr7jnmoTnhafniYfkuIrmjpLniYjlpJrlvKDor4Hku7bnhafmuIXljY7lpKflraborqHnrpfmnLrlrabnp5HmjqjojZDlrabmnK/kvJrorq7lkozmnJ/liIrliJfooahXb3JkUHJlc3Plk43lupTlvI/lhY3otLnkuLvpopggQXJ0IEJsb2fllK/lk4Hnp4DljZrlrqIgd2VpcHhpdSBjb20g5aSH55So5Z+f5ZCNd2VpcHhpdSBjbiDlvIDmupDnu5nlsI/kvJnkvLTlhY3otLnkvb/nlKgg5aaC5L2/55So6L+H56iL5pyJ5Lu75L2V6Zeu6aKYIOWcqOe6v+aKgOacr+aUr+aMgVFRIOasoui/juaJk+aJsCDljp/liJvkuI3mmJMg5aaC5Zac5qyiIOivt+WkmuWkmuaJk+i1jyDmvJTnpLogRXdvTWFpbOaYr+WfuuS6jkxpbnV455qE5LyB5Lia6YKu566x5pyN5Yqh5ZmoIOmbhuaIkOS6huS8l+WkmuS8mOengOeos+WumueahOe7hOS7tiDmmK/kuIDkuKrlv6vpgJ/pg6jnvbIg566A5Y2V6auY5pWIIOWkmuivreiogCDlronlhajnqLPlrprnmoTpgq7ku7bop6PlhrPmlrnmoYgg56yU6K6w5pys5paw54mI566A5Y2V5by65aSn55qE5peg5pWw5o2u5bqT55qE5Zu+5bqKMiDniYgg5ryU56S65Zyw5Z2AIEJpbGliaWxpIEIg56uZ6Ieq5Yqo6aKG55Oc5a2QIOebtOaSreWKqeaJiyDnm7Tmkq3mjILmnLrohJrmnKwg5Li756uZ5Yqp5omLIFBIUCDniYjlvq7kv6HnvqTkuoznu7TnoIHmtLvnoIHlt6Xlhbcg55Sf5oiQ5b6u5L+h576k5rS756CBIOmaj+aXtuWPr+S7peWIh+aNouS6jOe7tOeggSDnn63op4bpopHnmoRQSFDmi5PlsZXljIUg6ZuG5oiQ5ZCE5aSn55+t6KeG6aKR55qE5Y675rC05Y2w5Yqf6IO9IOaKlumfsyDlv6vmiYsg5b6u6KeG5Li75rWB55+t6KeG6aKRIFBIUOWOu+awtOWNsOS4gOS4qlBIUGVy55qE5Y2H57qn5LmL6Lev6YW355Oc5LqR6K++5aCCIOWcqOe6v+aVmeiCsiDnvZHor77ns7vnu58g572R5qCh57O757ufIOefpeivhuS7mOi0ueezu+e7nyDkuI3liqDlr4bkuI3pmInlibIgMSAl5YWo5Yqf6IO95byA5rqQIOWPr+WFjei0ueWVhueUqCDmoYbmnrbkuLvopoHkvb/nlKhUaGlua1BIUDYgbGF5dWkg5oul5pyJ5a6M5ZaE55qE5p2D6ZmQ55qE566h55CG5qih5Z2X5Lul5Y+K5pWP5o2355qE5byA5Y+R5pa55byPIOiuqeS9oOW8gOWPkei1t+adpeabtOWKoOeahOiIkuacjSBsYXJhdmVsNSA15pCt5bu655qE5ZCO5Y+w566h55CGIOWSjCBhcGnmnI3liqEg55qE5bCP56iL5bqP5ZWG5Z+O5Z+65LqOVGhpbmtQSFA1IEFkbWluTFRF55qE5ZCO5Y+w566h55CG57O757uf6a2U5pS554mI5pysIOS4uiBPTEFJTkRFWCDmt7vliqDlpJrnvZHnm5jmjILovb3lj4rkuIDkupvlsI/kv67lpI3mtbfosZpQSFAg5Z+65LqOVGhpbmtQSFA1IDEgNDFMVFPnmoTlv6vpgJ/lvIDlj5HmoYbmnrbmjILovb1UZWFtYml0aW9u5paH5Lu2IOWPr+ebtOmTvuWIhuS6qyDmlK/mjIHnvZHnm5gg6ZyA55Sz6K+3IOWSjOmhueebruaWh+S7tiDml6DpnIDpgoDor7fnoIEg5YeG56Gu546HOTkgOSXnmoRpcOWcsOWdgOWumuS9jeW6k2xhcmF2ZWwgYW50IGRlc2lnbiB2dWUg5p2D6ZmQ5ZCO5Y+wUEhQIOesrOS4ieaWueeZu+W9leaOiOadgyBTREsg6ZuG5oiQ5LqGUVEg5b6u5L+hIOW+ruWNmiBHaXRodWLnrYnluLjnlKjmjqXlj6Mg5pSv5oyBIHBocCBmcG0g5ZKMIFN3b29sZSDmiYDmnInmoYbmnrbpgJrnlKggUVHnvqQgMTc5MTYyMjfmipbpn7PljrvmsLTljbBQSFDniYjmjqXlj6PkuIDkuKrliIbluIPlvI/nu5/orqHnm5Hmjqfns7vnu58g5YyF5ZCrUEhQ5a6i5oi356uvIOacjeWKoeerr+aVtOWQiOWkmuaOpeWPo+eahElQ5p+l6K+i5bel5YW3IOWfuuS6jumYv+mHjOS6kU9TU+eahFdvcmRQcmVzc+i/nOeoi+mZhOS7tuaUr+aMgeaPkuS7tiDlkI7kvJrmnInmnJ8g5byA566x5Y2z55So55qETGFyYXZlbOWQjuWPsOaJqeWxlSDliY3lkI7nq6/liIbnprsg5ZCO56uv5o6n5Yi25YmN56uv57uE5Lu2IOaXoOmcgOe8luWGmXZ1ZeWNs+WPr+WIm+W7uuS4gOS4queahOmhueebriDkuLDlr4znmoTooajljZUg6KGo5qC857uE5Lu2IOW8uuWkp+eahOiHquWumuS5iee7hOS7tuWKn+iDvSB5aWkyIHN3b29sZSDorql5aWky6L+Q6KGM5Zyoc3dvb2xl5LiK6IOW6byg6YeH6ZuGIFdvcmRQcmVzc+S8mOengOW8gOa6kOmHh+mbhuaPkuS7tkNhdGNoQWRtaW7mmK/kuIDmrL7ln7rkuo50aGlua3BocDYg5ZKMIGVsZW1lbnQgYWRtaW4g5byA5Y+R55qE5ZCO5Y+w566h55CG57O757ufIOWfuuS6jiBTZXJ2aWNlUHJvdmlkZXIg57O757uf5qih5Z2X5a6M5YWo5o6l6ICmIOmaj+aXtuWNuOi9veWuieijheaooeWdlyDmj5DkvpvkuoblrozmlbTnmoTmnYPpmZDlkozmlbDmja7mnYPpmZDnrYnlip/og70g5aSn6YeP5YaF572u55qE5byA5Y+R5bel5YW35o+Q5Y2H5L2g55qE5byA5Y+R5L2T6aqMIOWumOe9keWcsOWdgCDlvq7kv6HlhazkvJflubPlj7BwaHDniYjlvIDlj5HljIXlvq7kv6HlsI/nqIvluo8g5qCh5Zut5bCP5oOF5Lmm5ZCO5Y+w5rqQ56CBIOWlveeOqeeahOihqOeZveWimSDlkYrnmb3lopkg5Yqf6IO95YWo6Z2i55qEUEhQ5ZG95Luk6KGM5bqU55So5bqTIOaPkOS+m+aOp+WItuWPsOWPguaVsOino+aekCDlkb3ku6Tov5DooYwg6aKc6Imy6aOO5qC86L6T5Ye6IOeUqOaIt+S/oeaBr+S6pOS6kiDnibnmrormoLzlvI/kv6Hmga/mmL7npLrln7rkuo4gY2hpbmVzZSBwb2V0cnkg5pWw5o2u5pW055CG55qE5LiA5Lu9IG15c3FsIOagvOW8j+aVsOaNruW4ruWKqSB0aGlua3BocCA1IOW8gOWPkeiAheW/q+mAnyDovbvmnb7nmoTmnoTlu7pBcGkgaHlwZXJmIGFkbWluIOaYr+WfuuS6jiBoeXBlcmYgdnVlIOeahOmFjee9ruWMluWQjuWPsOW8gOWPkeW3peWFtyDlvq7kv6HmlK/ku5hwaHAg5YaZ55qE6KeG6aKR5LiL6L295bel5YW3IOeOsOW3suaUr+aMgSBZb3VrdSBNaWFvcGFpIOiFvuiuryBYVmlkZW9zIFBvcm5odWIgOTFwb3JuIOW+ruWNmumFt+eHgyBiaWxpYmlsaSDku4rml6XlpLTmnaEg6IqS5p6cVFZDb3JlUHJlc3Mg5Li76aKYIOS4gOasvumrmOaAp+iDvSDpq5jpopzlgLznmoRXb3JkUHJlc3PkuLvpopjlv6vpk77nlLXllYYg55u05pKt55S15ZWGIOWIhumUgOWVhuWfjiDlvq7kv6HlsI/nqIvluo/llYbln44gQVBQ5ZWG5Z+OIOWFrOS8l+WPt+WVhuWfjiBQQ+WVhuWfjuezu+e7nyDmlK/ku5jlrp3lsI/nqIvluo/llYbln44g5oqW6Z+z5bCP56iL5bqP5ZWG5Z+OIOeZvuW6puWwj+eoi+W6j+eUteWVhuezu+e7nyDliY3lkI7nq6/ku6PnoIHlhajpg6jlvIDmupAgTGFyYXZlbCB2dWXlvIDlj5Eg5oiQ54af5ZWG55So6aG555uuIHNob3AgbWFsbCDllYbln44g55S15ZWGIOWIqeeUqCBQSFAgY1VSTCDovazlj5EgRGlzcXVzIEFQSSDor7fmsYLlj6/og73mmK/mnIDkvJjpm4Ug566A5piT55qE5reY5a6d5a6iU0RLVW5pQWRtaW7mmK/kuIDlpZfmuJDov5vlvI/mqKHlnZfljJblvIDmupDlkI7lj7Ag6YeH55So5YmN5ZCO56uv5YiG56a75oqA5pyvIOaVsOaNruS6pOS6kumHh+eUqGpzb27moLzlvI8g5Yqf6IO95L2O6ICm5ZCI6auY5YaF6IGaIOaguOW/g+aooeWdl+aUr+aMgeezu+e7n+iuvue9riDmnYPpmZDnrqHnkIYg55So5oi3566h55CGIOiPnOWNleeuoeeQhiBBUEnnrqHnkIbnrYnlip/og70g5ZCO5pyf5LiK57q/5qih5Z2X5ZWG5Z+O5bCG5omT6YCg57G75Ly8Y29tcG9zZXIgbnBt55qE5byA5pS+5byP5o+S5Lu25biC5Zy6IOWQjOaXtuaIkeS7rOWwhuaJk+mAoOS4gOWll+WFvOWuueaAp+eahEFQSeagh+WHhiDku45UaGlua1BIUDUgMSBWdWUy5byA5aeLIOmAkOatpeWQuOW8leeIseWlveiAheWFseWQjOWKoOWFpSDku6Xopobnm5bnrYnlpJror63oqIDmoYbmnrYgUEhQIOWkmuaOpeWPo+iOt+WPluW/q+mAkueJqea1geS/oeaBr+WMhUxpZ2h0Q01TIOaYr+S4gOS4quWfuuS6jiBMYXJhdmVsIOW8gOWPkeeahOi9u+mHj+e6pyBDTVMg57O757ufIOS5n+WPr+S7peS9nOS4uuS4gOS4qumAmueUqOeahOWQjuWPsOeuoeeQhuahhuaetuS9v+eUqOWNleeCueeZu+W9leezu+e7n+W/q+S5kOS6jOe6p+Wfn+WQjeWIhuWPkeezu+e7n1R5cGVjaG8gVGhlbWUgU3Rvcnkg54ix5LiK5L2g5oiR55qE5pWF5LqLIOS4gOS4qui9u+mHj+WMlueahOeVmeiogOadvyDorrDkuovmnKwg56S+5Lqk57O757ufIOWNmuWuoiDkurrnsbvnmoTmnKzotKjmmK8g5ZKV5ZKV5ZKV77yf5b6u5L+h5Z+f5ZCN5oum5oiq5qOA5rWLIFFR5Z+f5ZCN5oum5oiq5qOA5rWLIHQgeHpreGIgY29tIOafpeivouaciee8k+WtmCDlpoLpnIDlrp7ml7bmn6Xor6Lor7foh6rooYzpg6jnvbIg6auY5oCn6IO95YiG5biD5byP5bm25Y+R6ZSBIOihjOS4uumZkOa1gUVtbG9n5piv5LiA5qy+5Z+65LqOUEhQ5ZKMTXlTUUznmoTlip/og73lvLrlpKfnmoTljZrlrqLlj4pDTVPlu7rnq5nns7vnu58g6L+95rGC5b+r6YCfIOeos+WumiDnroDljZUg6IiS6YCC55qE5bu656uZ5L2T6aqMSHlwZXJmIGFkbWluIOWfuuS6jkh5cGVyZiBFbGVtZW50IFVJIOmAmueUqOeuoeeQhuWQjuWPsOS8geS4muS7k+W6k+euoeeQhuezu+e7n0hpc2lQSFAgVjLniYjmmK/ln7rkuo5UaGlua1BIUDUgMeWSjExheXVp5byA5Y+R55qE5ZCO5Y+w5qGG5p62IOaJv+ivuuawuOS5heWFjei0ueW8gOa6kCDmgqjlj6/nlKjkuo7lrabkuaDlkozllYbnlKgg5L2G6aG75L+d55WZ54mI5p2D5L+h5oGv5q2j5bi45pi+56S6IOWmguaenEhpc2lQSFDlr7nmgqjmnInluK7liqkg5oKo5Y+v5Lul54K55Ye75Y+z5LiK6KeSIFN0YXIg5pSv5oyB5LiA5LiL5ZOmIOiwouiwoiDkvb/nlKhQSFDlvIDlj5HnmoTnroDnuqblr7zoiKog5Lmm562+566h55CG57O757ufIOi9r+aTjuaYr+WfuuS6jiBQaHAgNyAyIOWSjCBTd29vbGUgNCA0IOeahOmrmOaAp+iDvSDnroDljZXmmJPnlKjnmoTlvIDlj5HmoYbmnrYg5pSv5oyB5ZCM5pe25ZyoIFN3b29sZSBTZXJ2ZXIg5ZKMIHBocCBmcG0g5Lik56eN5qih5byP5LiL6L+Q6KGMIOWGhee9ruS6huacjeWKoSDpm4bmiJDkuoblpKfph4/miJDnhp/nmoTnu4Tku7Yg5Y+v5Lul55So5LqO5p6E5bu66auY5oCn6IO955qEV2Vi57O757ufIEFQSSDkuK3pl7Tku7Yg5Z+656GA5pyN5Yqh562J562JIOS4quS6uuWPkeWNoea6kOeggSDlj5HljaHns7vnu58g5LqM5qyh5YWD5Y+R5Y2h57O757ufIOS6jOasoeWFg+WPkeWNoea6kOeggSDlj5HljaHnqIvluo8g5Yqo5ryr5Y+R5Y2hIFBIUOWPkeWNoea6kOeggeiBiuWkqeW6lOeUqCBwaHDlrp7njrDnmoRkaHTniKzomavmkK3lu7rnmoR3ZWJpbeWuouacjeezu+e7nyDljbPml7bpgJrorq/kuIDkupvlrp7nlKjnmoRweXRob27ohJrmnKzlkIzln47mi7zovablvq7kv6HlsI/nqIvluo/lkI7nq6/ku6PnoIEg5LiA5Liq5ZON5bqU5byP5bmy5YeA5ZKM566A5rSB5LyY6ZuF55qEIFR5cGVjaG8g5Li76aKYcGhw5LuT5bqT6L+b6ZSA5a2Y5rex5bqm5a2m5LmgNSDpl64g5Lul6Zeu562U5b2i5byP5a+55bi455So55qE5qaC546H55+l6K+GIOe6v+aAp+S7o+aVsCDmnLrlmajlrabkuaAg5rex5bqm5a2m5LmgIOiuoeeul+acuuinhuinieetieeDreeCuemXrumimOi/m+ihjOmYkOi/sCDku6XluK7liqnoh6rlt7Hlj4rmnInpnIDopoHnmoTor7vogIUg5YWo5Lmm5YiG5Li6MTjkuKrnq6DoioIgNSDkvZnkuIflrZcg55Sx5LqO5rC05bmz5pyJ6ZmQIOS5puS4reS4jeWmpeS5i+WkhOaBs+ivt+W5v+Wkp+ivu+iAheaJueivhOaMh+atoyDmnKrlrozlvoXnu60g5aaC5pyJ5oSP5ZCI5L2cIOiBlOezu3NjdXRqeTIgMTUgMTYzIGNvbSDniYjmnYPmiYDmnIkg6L+d5p2D5b+F56m2IFRhbiAyIDE4IDbpopjop6Mg6K6w5b2V6Ieq5bex55qEbGVldGNvZGXop6PpopjkuYvot68g5pyA5YWo5Lit5Y2O5Y+k6K+X6K+N5pWw5o2u5bqTIOWUkOWui+S4pOacnei/keS4gOS4h+Wbm+WNg+WPpOivl+S6uiDmjqXov5E1IDXkuIfpppbllJDor5fliqAyNuS4h+Wui+ivlyDkuKTlrovml7bmnJ8xNTY05L2N6K+N5Lq6IDIxIDUg6aaW6K+NIHVuaSBhcHAg5piv5L2/55SoIFZ1ZSDor63ms5XlvIDlj5HlsI/nqIvluo8gSDUgQXBw55qE57uf5LiA5qGG5p626YeH55So6Ieq6Lqr5qih5Z2X6KeE6IyD57yW5YaZ55qE5YmN56uvIFVJIOahhuaetiDpgbXlvqrljp/nlJ8gSFRNTCBDU1MgSlMg55qE5Lmm5YaZ5b2i5byPIOaegeS9jumXqOanmyDmi7/mnaXljbPnlKgg5oiR5piv5L6d5omsIOacqOaYk+adqCDlhazkvJflj7cg6auY57qn5YmN56uv6L+b6Zi2IOS9nOiAhSDmr4/lpKnmkJ7lrprkuIDpgZPliY3nq6/lpKfljoLpnaLor5Xpopgg56Wd5aSn5a625aSp5aSp6L+b5q2lIOS4gOW5tOWQjuS8mueci+WIsOS4jeS4gOagt+eahOiHquW3sSBZQXBpIOaYr+S4gOS4quWPr+acrOWcsOmDqOe9sueahCDmiZPpgJrliY3lkI7nq6/lj4pRQeeahCDlj6/op4bljJbnmoTmjqXlj6PnrqHnkIblubPlj7DlsI/nqIvluo/nu4Tku7bljJblvIDlj5HmoYbmnrbnvZHmmJPkupHpn7PkuZAgTm9kZSBqcyBBUEkgc2VydmljZeWfuuS6jiBWdWUganMg55qE5bCP56iL5bqP5byA5Y+R5qGG5p62IOS7juW6leWxguaUr+aMgSBWdWUganMg6K+t5rOV5ZKM5p6E5bu65bel5YW35L2T57O7IEVDTUFTY3JpcHQgNuWFpemXqCDmmK/kuIDmnKzlvIDmupDnmoQgSmF2YVNjcmlwdCDor63oqIDmlZnnqIsg5YWo6Z2i5LuL57uNIEVDTUFTY3JpcHQgNiDmlrDlop7nmoTor63ms5XnibnmgKcg6LC357KSIENocm9tZeaPkuS7tuiLsembhOamnCDkuLrkvJjnp4DnmoRDaHJvbWXmj5Lku7blhpnkuIDmnKzkuK3mlofor7TmmI7kuaYg6K6pQ2hyb21l5o+S5Lu26Iux6ZuE5Lus6YCg56aP5Lq657G75YWs5LyX5Y+3IOWKoDEg5ZCM5q2l5pu05paw5YmN56uv6Z2i6K+V5q+P5pelIDMgMSDku6XpnaLor5XpopjmnaXpqbHliqjlrabkuaAg5o+Q5YCh5q+P5pel5a2m5Lmg5LiO5oCd6ICDIOavj+Wkqei/m+atpeS4gOeCuSDmr4/lpKnml6nkuIo154K557qv5omL5bel5Y+R5biD6Z2i6K+V6aKYIOatu+ejleiHquW3sSDmhInmgqblpKflrrYgNCDpgZPliY3nq6/pnaLor5XpopjlhajpnaLopobnm5blsI/nqIvluo8g6L2v5oqA6IO9IOacrOaWh+WOn+aWh+eUseefpeWQjSBIYWNrZXIgRXJpYyBTIFJheW1vbmQg5omA5pKw5a+rIOaVmeS9oOWmguS9leato+eiuueahOaPkOWHuuaKgOihk+WVj+mhjOS4pueNsuW+l+S9oOa7v+aEj+eahOetlOahiCDljYPlj6TliY3nq6/lm77mlofmlZnnqIsg6LaF6K+m57uG55qE5YmN56uv5YWl6Zeo5Yiw6L+b6Zi25a2m5Lmg56yU6K6wIOS7jumbtuW8gOWni+WtpuWJjeerryDlgZrkuIDlkI3nsr7oh7TkvJjpm4XnmoTliY3nq6/lt6XnqIvluIgg5YWs5LyX5Y+3IOWNg+WPpOWjueWPtyDkvZzogIUgYm9vayBOb2RlIGpzIOWMheaVmeS4jeWMheS8miBieSBhbHNvdGFuZ+aUtumbhuaJgOacieWMuuWdl+mTviBCbG9ja0NoYWluIOaKgOacr+W8gOWPkeebuOWFs+i1hOaWmSDljIXmi6xGYWJyaWPlkoxFdGhlcmV1beW8gOWPkei1hOaWmei9u+mHjyDlj6/pnaDnmoTlsI/nqIvluo8gVUkg57uE5Lu25bqT5b6u5L+h5bCP56iL5bqP5ZWG5Z+OIOW+ruS/oeWwj+eoi+W6j+W+ruW6l+S4gOS4quWPr+S7peingueci+WbveWGheS4u+a1geinhumikeW5s+WPsOaJgOacieinhumikeeahOWuouaIt+err+WPr+S8uOe8qeW4g+WxgOaWueahiOWfuuS6jiBub2RlIGpzIE1vbmdvZGIg5p6E5bu655qE5ZCO5Y+w57O757ufIGpzIOa6kOeggeino+aekOejgeWKm+mTvuaOpeiBmuWQiOaQnOe0ouS4reWNjuS6uuawkeWFseWSjOWbveihjOaUv+WMuuWIkiDnnIHnuqcg55yB5Lu955u06L6W5biC6Ieq5rK75Yy6IOWcsOe6pyDln47luIIg5Y6/57qnIOWMuuWOvyDkuaHnuqcg5Lmh6ZWH6KGX6YGTIOadkee6pyDmnZHlp5TkvJrlsYXlp5TkvJog5Lit5Zu955yB5biC5Yy66ZWH5p2R5LqM57qn5LiJ57qn5Zub57qn5LqU57qn6IGU5Yqo5Zyw5Z2A5pWw5o2uIFdlYuaOpeWPo+euoeeQhuW3peWFtyDlvIDmupDlhY3otLkg5o6l5Y+j6Ieq5Yqo5YyWIE1PQ0vmlbDmja7oh6rliqjnlJ/miJAg6Ieq5Yqo5YyW5rWL6K+VIOS8geS4mue6p+euoeeQhiDpmL/ph4zlpojlpohNVVjlm6LpmJ/lh7rlk4Eg6Zi/6YeM5be05be06YO95Zyo55SoIDEg5YWs5Y+455qE6YCJ5oupIFJBUDLlt7Llj5HluIPor7fnp7vmraXoh7NnaXRodWIgY29tIHRoeCByYXAyIGRlbG9zS3Vib2FyZCDmmK/ln7rkuo4gS3ViZXJuZXRlcyDnmoTlvq7mnI3liqHnrqHnkIbnlYzpnaIg5ZCM5pe25o+Q5L6bIEt1YmVybmV0ZXMg5YWN6LS55Lit5paH5pWZ56iLIOWFpemXqOaVmeeoiyDmnIDmlrDniYjmnKznmoQgS3ViZXJuZXRlcyB2MSAyIOWuieijheaJi+WGjCBrOHMgaW5zdGFsbCDlnKjnur/nrZTnlpEg5oyB57ut5pu05pawIEFwYWNoZUNOIOaVsOaNrue7k+aehOS4jueul+azleivkeaWh+mbhiBjaGljayDmmK/kvb/nlKggTm9kZSBqcyDlkowgTW9uZ29EQiDlvIDlj5HnmoTnpL7ljLrns7vnu5/kuIDkuKrpnZ7luLjpgILlkIhJVOWboumYn+eahOWcqOe6v0FQSeaWh+ahoyDmioDmnK/mlofmoaPlt6XlhbcgQ2hpbmVzZSBzdGlja2VyIHBhY2sgTW9yZSBqb3kg6KGo5oOF5YyF55qE5Y2a54mp6aaGIEdpdGh1YuacgOacieavkueahOS7k+W6kyDkuK3lm73ooajmg4XljIXlpKfpm4blkIgg6IGa5qyi5LmQIOmrmOminOWAvOeahOesrOS4ieaWuee9keaYk+S6keaSreaUvuWZqCDmlK/mjIEgV2luZG93cyBtYWNPUyBMaW51eCB2dWUyIHZ1ZSByb3V0ZXIgdnVleCDlhaXpl6jpobnnm67nvZHmmJPkupHpn7PkuZDnrKzkuInmlrkgRmx1dHRlcuWunuaImCDnlLXlrZDkuaYg5LiA5aWX5Luj56CB6L+Q6KGM5aSa56uvIOS4gOerr+aJgOingeWNs+Wkmuerr+aJgOingSDorqHnrpfmnLrpgJ/miJDor74gQ3Jhc2ggQ291cnNlIOWtl+W5lee7hCDlhag0IOmbhiAyIDE4IDUgMSDnsr7moKHlrozmiJAg5LiA5LiqIHJlYWN0IHJlZHV4IOeahOWujOaVtOmhueebriDlkowg5Liq5Lq65oC757uT5Lit5paH54us56uL5Y2a5a6i5YiX6KGoQ1NTIEluc3BpcmF0aW9uIOWcqOi/memHjOaJvuWIsOWGmSBDU1Mg55qE54G15oSfIHJpY2ggdGV4dCDlr4zmlofmnKznvJbovpHlmagg5rGJ5a2X5ou86Z+zIGjDoG4gesOsIHDEq24gecSrbiBDaHJvbWXmj5Lku7blvIDlj5HlhajmlLvnlaUg6YWN5aWX5a6M5pW0RGVtbyDmrKLov45jbG9uZeS9k+mqjOW+ruS/oeiwg+ivlSDlkITnp41XZWJWaWV35qC35byP6LCD6K+VIOaJi+acuua1j+iniOWZqOeahOmhtemdouecn+acuuiwg+ivlSDkvr/mjbfnmoTov5znqIvosIPor5XmiYvmnLrpobXpnaIg5oqT5YyF5bel5YW3IOaUr+aMgSBIVFRQUyDml6DpnIBVU0Lov57mjqXorr7lpIcgbWFzdGVy5YiG5pSvIOa4suafk+WZqCDlvq7kv6HlsI/nqIvluo/nu4Tku7YgQVBJIOS6keW8gOWPkeekuuS+i+eugOaCpiBTaW1wUmVhZCDorqnkvaDnnqzpl7Tov5vlhaXmsonmtbjlvI/pmIXor7vnmoTmianlsZXorqlINeWItuS9nOWDj+aQreenr+acqOS4gOagt+eugOWNlSDovbvmnb7mkK3lu7pINemhtemdoiBINee9keermSBQQ+err+e9keermSBMb3dDb2Rl5bmz5Y+wIOS4gOWll+e7hOS7tuWMliDlj6/lpI3nlKgg5piT5omp5bGV55qE5b6u5L+h5bCP56iL5bqPIFVJIOe7hOS7tuW6k+i/meaYr+S4gOS4quaVsOaNruWPr+inhuWMlumhueebriDog73lpJ/lsIbljoblj7LmlbDmja7mjpLlkI3ovazljJbkuLrliqjmgIHmn7Hnirblm77lm77ooajlvq7kv6HlsI/nqIvluo/lm77ooahjaGFydHPnu4Tku7YgQ2hhcnRzIGZvciBXZUNoYXQgc21hbGwgYXBw57G75Ly85piT5LyB56eA55qESDXliLbkvZwg5bu656uZ5bel5YW3IOWPr+inhuWMluaQreW7uuezu+e7nyDkuIDkuKrlnKjkvaDnvJbnqIvml7bnlq/ni4Lnp7DotZ7kvaDnmoQgVlNDb2RlIOaJqeWxleaPkuS7tuWFqOWutuahtuWQjuWPsOeuoeeQhuahhuaetuino+mUgee9keaYk+S6kemfs+S5kOWuouaIt+err+WPmOeBsOatjOabsue+juinguaYk+eUqOeahFJlYWN05a+M5paH5pys57yW6L6R5ZmoIOWfuuS6jmRyYWZ0IGpz5byA5Y+R5LiA5Liq6Ie05Yqb5LqO5b6u5L+h5bCP56iL5bqP5ZKMIFdlYiDnq6/lkIzmnoTnmoTop6PlhrPmlrnmoYjlvp7pm7bplovlp4vlrbggUmVhY3RKUyBSZWFjdEpTIDEgMSDmmK/kuIDmnKzluIzmnJvorpPliJ3lrbjogIXkuIDnnIvlsLHmh4LnmoQgUmVhY3Qg5Lit5paH5YWl6ZaA5pWZ5a245pu4IOeUsea3uuWFpea3seWtuOe/kiBSZWFjdEpTIOeUn+aFi+ezu+a6kOeggeino+ivuyDns7vliJfmlofnq6Ag5a6MIOaIkeWwseaYr+adpeWIhuS6q+iEmuacrOeOqeeOqeeahOW8gOWPkeiAhei+uei9piBnaXRodWLmiZPkuI3lvIAgZ2l0aHVi5Yqg6YCfIGdpdCBjbG9uZeWKoOmAnyBnaXQgcmVsZWFzZeS4i+i9veWKoOmAnyBzdGFja292ZXJmbG935Yqg6YCfdnVl5rqQ56CB6YCQ6KGM5rOo6YeK5YiG5p6QIDQg5aSabeeahHZ1Zea6kOeggeeoi+W6j+a1geeoi+WbvuaAnee7tOWvvOWbviBkaWZm6YOo5YiG5b6F5ZCO57ut5pu05pawIOW+ruS/oeWwj+eoi+W6j+ino+WGs+aWueahiCAxS0IgamF2YXNjcmlwdCDopobnm5bnirbmgIHnrqHnkIYg6Leo6aG16YCa6K6vIOaPkuS7tuW8gOWPkeWSjOS6keaVsOaNruW6k+W8gOWPkee7meiAgeWPuOacuueUqOeahOS4gOS4queVquWPt+aOqOiNkOezu+e7nyBGZUhlbHBlciBXZWLliY3nq6/liqnmiYvorrDlvZXmiJDplb/nmoTov4fnqIvlk5Tlk6nlk5Tlk6kgYmlsaWJpbGkgY29tIOi+heWKqeW3peWFtyDlj6/ku6Xmm7/mjaLmkq3mlL7lmagg5o6o6YCB6YCa55+l5bm26L+b6KGM5LiA5Lqb5b+r5o235pON5L2c5o+Q5L6b5LqG55m+5bqm5Z2Q5qCHIEJEIDkg5Zu95rWL5bGA5Z2Q5qCHIOeBq+aYn+WdkOaghyBHQ0ogMiDlkoxXR1M4NOWdkOagh+ezu+S5i+mXtOeahOi9rOaNokYyZXRlc3TmmK/kuIDkuKrpnaLlkJHliY3nq68g5rWL6K+VIOS6p+WTgeetieWyl+S9jeeahOWkmua1j+iniOWZqOWFvOWuueaAp+a1i+ivleaVtOS9k+ino+WGs+aWueahiCDvuI8g6Zi/6YeM6aOe54yqIOW+iOaYk+eUqOeahOS4reWQjuWPsCDooajljZUg6KGo5qC8IOWbvuihqCDop6PlhrPmlrnmoYhDUk1FQiBNaW4g5YmN5ZCO56uv5YiG56a754mI6Ieq5bim5a6i5pyN57O757ufIOaYr0NSTUVC5ZOB54mM5YWo5paw5o6o5Ye655qE5LiA5qy+6L276YeP57qnIOmrmOaAp+iDvSDliY3lkI7nq6/liIbnprvnmoTlvIDmupDnlLXllYbns7vnu58g5a6M5ZaE55qE5ZCO5Y+w5p2D6ZmQ566h55CGIOS8muWRmOeuoeeQhiDorqLljZXnrqHnkIYg5Lqn5ZOB566h55CGIOWuouacjeeuoeeQhiBDTVPnrqHnkIYg5aSa56uv566h55CGIOmhtemdokRJWSDmlbDmja7nu5/orqEg57O757uf6YWN572uIOe7hOWQiOaVsOaNrueuoeeQhiDml6Xlv5fnrqHnkIYg5pWw5o2u5bqT566h55CGIOS4gOmUruW8gOmAmuefreS/oSDkuqflk4Hph4fpm4Yg54mp5rWB5p+l6K+i562J5o6l5Y+jIFJlYWN05oqA5pyv5o+t56eYIOS4gOacrOiHqumhtuWQkeS4i+eahFJlYWN05rqQ56CB5YiG5p6Q5Lmm5b6u5L+h5bCP56iL5bqPIOWfuuS6jndlcHkg5ZWG5Z+OIOW+ruW6lyDlvq7kv6HlsI/nqIvluo8g5qyi6L+O5a2m5Lmg5Lqk5rWB5aSn5bGP5pWw5o2u5Y+v6KeG5YyWUHl0b3JjaCDkuK3mlofmlofmoaPnu4/lhbjnmoTnvZHpobXlr7nor53moYbnu4Tku7Yg5by65aSn55qE5Yqo5oCB6KGo5Y2V55Sf5oiQ5ZmoIOeugOa0gSDmmJPnlKgg54G15rS755qE5b6u5L+h5bCP56iL5bqP57uE5Lu25bqTIOS4gOasviBNYXRlcmlhbCBEZXNpZ24g6aOO5qC855qEIEhleG8g5Li76aKY562+5Yiw5LiA5Liq5biu5Yqp5L2g6Ieq5Yqo55Sz6K+35Lqs5Lic5Lu35qC85L+d5oqk55qEY2hyb21l5ouT5bGV5ZCO5Y+wYWRtaW7liY3nq6/mqKHmnb8g5Z+65LqOIGxheXVpIOe8luWGmeeahOacgOeugOa0gSDmmJPnlKjnmoTlkI7lj7DmoYbmnrbmqKHmnb8g5Y+q6ZyA5o+Q5L6b5LiA5Liq5o6l5Y+j5bCx55u05o6l5Yid5aeL5YyW5pW05Liq5qGG5p62IOaXoOmcgOWkjeadguaTjeS9nCDlsI/nqIvluo/nlJ/miJDlm77niYflupMg6L275p2+6YCa6L+HIGpzb24g5pa55byP57uY5Yi25LiA5byg5Y+v5Lul5Y+R5Yiw5pyL5Y+L5ZyI55qE5Zu+54mH5a6J5Y2T5bqU55So5bGC5oqT5YyF6YCa5p2A6ISa5pys5LiA5Liq6L276YeP55qE5bel5YW36ZuG5ZCI5ama56S85aSn5bGP5LqS5YqoIOW+ruS/oeivt+afrOS4gOermeW8j+ino+WGs+aWueahiG1pbGkg5piv5LiA5Liq5byA5rqQ55qE56S+5Yy657O757ufIOeVjOmdouS8mOmbhSDlip/og73kuLDlr4wg5Lid6Iis6aG65ruR55qE6Kem5pG46L+Q5Yqo5pa55qGI5YGa5pyA5aW955qE5o6l5Y+j566h55CG5bmz5Y+wTXB4IOS4gOasvuWFt+acieS8mOengOW8gOWPkeS9k+mqjOWSjOa3seW6puaAp+iDveS8mOWMlueahOWinuW8uuWei+i3qOerr+Wwj+eoi+W6j+ahhuaetuecgeW4guWMuuWOv+S5oemVh+S4iee6p+aIluWbm+e6p+WfjuW4guaVsOaNriDluKbmi7zpn7PmoIfms6gg5Z2Q5qCHIOihjOaUv+WMuuWfn+i+ueeVjOiMg+WbtCAyIDIx5bm0IDfmnIggM+aXpeacgOaWsOmHh+mbhiDmj5Dkvptjc3bmoLzlvI/mlofku7Yg5pSv5oyB5Zyo57q/6L2s5oiQ5aSa57qn6IGU5YqoanPku6PnoIEg6YCa55SoanNvbuagvOW8jyDmj5Dkvpvova/ku7bovazmiJBzaHAgZ2VvanNvbiBzcWwg5a+85YWl5pWw5o2u5bqTIOW4pua1j+iniOWZqOmHjOmdoui/kOihjOeahGpz6YeH6ZuG5rqQ56CBIOe7vOWQiOS6huS4reWNjuS6uuawkeWFseWSjOWbveawkeaUv+mDqCDlm73lrrbnu5/orqHlsYAg6auY5b635Zyw5Zu+IOiFvuiur+WcsOWbvuihjOaUv+WMuuWIkuaVsOaNruWcqHZzY29kZeS4reeUqOS6jueUn+aIkOaWh+S7tuWktOmDqOazqOmHiuWSjOWHveaVsOazqOmHiueahOaPkuS7tiDnu4/ov4flpJrniYjov63ku6PlkI4g5o+S5Lu2IOaUr+aMgeaJgOacieS4u+a1geivreiogCDlip/og73lvLrlpKcg54G15rS75pa55L6/IOaWh+aho+m9kOWFqCDpo5/nlKjnroDljZUg6KeJ5b6X5o+S5Lu25LiN6ZSZ55qE6K+dIOeCueWHu+WPs+S4iuinkue7meS4qlN0YXIg77iP5ZGAIEpBVkNsdWIg6K6p5L2g55qE5aSn5aeQ5aeQ5LiN5YaN6LWw5Lii77iP5L2g5oOz6KaB55qE5pyA5YWoIEFuZHJvaWQg6L+b6Zi26Lev57q/55+l6K+G5Zu+6LCxIOW5sui0p+i1hOaWmeaUtumbhiDlvIDlj5HogIXmjqjojZDpmIXor7vnmoTkuabnsY0gMiAyIOa3mOWunSDkuqzkuJwg5pSv5LuY5a6d5Y+M5Y2B5LiAIOWPjDEx5YWo5rCR5YW754yrIOWFqOawkeiQpeS4muiHquWKqOWMluiEmuacrCDlhajpop3lpZblirEg6Ziy5qOA5rWLIOS4gOasvumrmOaAp+iDveaVj+aEn+ivjSDpnZ7ms5Xor40g6ISP5a2XIOajgOa1i+i/h+a7pOe7hOS7tiDpmYTluKbnuYHkvZPnroDkvZPkupLmjaIg5pSv5oyB5YWo6KeS5Y2K6KeS5LqS5o2iIOaxieWtl+i9rOaLvOmfsyDmqKHns4rmkJzntKLnrYnlip/og70g5YmN56uv5Y2a5a6iIOWFs+azqOWfuuehgOefpeivhuWSjOaAp+iDveS8mOWMliB2dWUgY2xpNOmFjee9rnZ1ZSBjb25maWcganPmjIHnu63mm7TmlrBQVCDliqnmiYsgUGx1cyDkuLogR29vZ2xlIENocm9tZSDlkowgRmlyZWZveCDmtY/op4jlmajmj5Lku7YgV2ViIEV4dGVuc2lvbnMg5Li76KaB55So5LqO6L6F5Yqp5LiL6L29IFBUIOermeeahOenjeWtkCDln7rkuo52dWUyIGtvYTLnmoQgSDXliLbkvZzlt6Xlhbcg6K6p5LiN5Lya5YaZ5Luj56CB55qE5Lq65Lmf6IO96L275p2+5b+r6YCf5LiK5omL5Yi25L2cSDXpobXpnaIg57G75Ly85piT5LyB56eAIOeZvuW6pkg1562JSDXliLbkvZwg5bu656uZ5bel5YW35pyA5YWo5pyA5paw5Lit5Zu955yBIOW4giDlnLDljLpqc29u5Y+Kc3Fs5pWw5o2u6aaW5LiqIFRhcm8g5aSa56uv57uf5LiA5a6e5L6LIOe9keaYk+S4pemAiSDlsI/nqIvluo8gSDUgUmVhY3QgTmF0aXZlIEJ5IOi2o+W6lyBGRUTlnLDnkIbkv6Hmga/lj6/op4bljJblupPkvIHkuJrnuqcgTm9kZSBqcyDlupTnlKjmgKfog73nm5HmjqfkuI7nur/kuIrmlYXpmpzlrprkvY3op6PlhrPmlrnmoYggTm9kZSBqc+WMuuWdl+mTvuW8gOWPkSDms6gg5paw54mI5Luj56CB5bey5byA5rqQIOivt3N0YXLmlK/mjIHlk6Yg5Z+65LqOQXV0byBqc+eahOiaguiageajruael+iDvemHj+iHquWKqOaUtuWPluiEmuacrCDnu5Plt7Qg5Lit5paH5YiG6K+N55qETm9kZSBqc+eJiOacrCDor5Eg6Z2i5ZCR5py65Zmo5a2m5Lmg55qE54m55b6B5bel56iLd2ViZnVubnnmmK/kuIDmrL7ovbvph4/nuqfnmoTliY3nq6/nm5Hmjqfns7vnu58gd2ViZnVubnnkuZ/mmK/kuIDmrL7liY3nq6/mgKfog73nm5Hmjqfns7vnu58g5peg5Z+L54K555uR5o6n5YmN56uv5pel5b+XIOWunuaXtuWIhuaekOWJjeerr+WBpeW6t+eKtuaAgeS4gOS4quWunueOsOaxieWtl+S4juaLvOmfs+S6kui9rOeahOWwj+W3p3dlYuW3peWFt+W6kyDmvJTnpLrlnLDlnYAg5YmN56uv6L+b6Zi2IOS8mOi0qOWNmuaWhyDkuIDkuKogQ2hyb21lIOaPkuS7tiDlsIYgR29vZ2xlIENETiDmm7/mjaLkuLrlm73lhoXnmoQgVnVlIEVsZW1lbnRVSeaehOW7uueahENNU+W8gOWPkeahhuaetui2heWujOaVtOeahFJlYWN0IE5hdGl2ZemhueebriDlip/og73kuLDlr4wg6YCC5ZCI5a2m5Lmg5ZKM5pel5bi45L2/55SoIEdTWUdpdGh1YkFwcOezu+WIl+eahOS8mOWKvyDmiJHku6znm67liY3lt7Lnu4/mi6XmnInlm5vkuKrniYjmnKwg5Yqf6IO96b2Q5YWoIOmhueebruahhuaetuWGheaKgOacr+a2ieWPiumdouW5vyDlrozmiJDluqbpq5gg6YWN5aWX5paH56ugIOmAguWQiOWFqOmdouWtpuS5oCDlr7nmr5Tlj4LogIMg5byA5rqQR2l0aHVi5a6i5oi356uvQXBwIOabtOWlveeahOS9k+mqjCDmm7TkuLDlr4znmoTlip/og70g5peo5Zyo5pu05aW955qE5pel5bi4566h55CG5ZKM57u05oqk5Liq5Lq6R2l0aHViIOaPkOS+m+abtOWlveabtOaWueS+v+eahOmpvui9puS9k+mqjM6jIOWQjOasvldlZXjniYjmnKzlkIzmrL5GbHV0dGVy54mI5pysIGh0dHBzIGdpdGh1YiBjb20gQ2FyR3Ug576kIOWuh+WumeacgOW8uueahOWJjeerr+mdouivleaMh+WNlyBsdWNpZmVyIHJlbiBmZSBpbnRlcnZpZXcg5b6u5oWV5bCP56iL5bqP5byA5rqQ54mIIFdvcmRQcmVzc+eJiOW+ruS/oeWwj+eoi+W6j+WHveaVsOW8j+e8lueoi+aMh+WMl+S4reaWh+eJiE5vZGUganPpnaLor5Xpopgg5L6n6YeN5ZCO56uv5bqU55So5LiO5a+5Tm9kZeaguOW/g+eahOeQhuino2pRdWVyeea6kOeggeino+aekOWwj+eZveWFpeWdkXZ1ZeS4iemDqOabsiDlhbPkuo4gdnVlIOWcqOW3peS9nOeahOS9v+eUqOmXrumimOaAu+e7kyDor7fnnIvljZrlrqIgc3Vuc2Vla2VycyBnaXRodWIgaW8g5LiA55u05L+d5oyB5pu05paw5Z+65LqOIFZ1ZSDnmoQgUFdBIOino+WGs+aWueahiCDluK7liqnlvIDlj5HogIXlv6vpgJ/mkK3lu7ogUFdBIOW6lOeUqCDop6PlhrPmjqXlhaUgUFdBIOeahOWQhOenjemXrumimFRoaW5rQ01G5piv5LiA5qy+5pSv5oyBU3dvb2xl55qE5byA5rqQ5YaF5a65566h55CG5qGG5p62IOWfuuS6jlRoaW5rUEhQ5byA5Y+RIOWQjOaXtuaUr+aMgVBIUCBGUE3lkoxTd29vbGXlj4zmqKHlvI8g6K6pV0VC5byA5Y+R5pu05b+rIOW+ruS/oeWwj+eoi+W6j+WbvueJh+ijgeWJquW3peWFt+WJjeerr+efpeivhuaciOWIik5leHQgVGVybWluYWzmmK/kuIDkuKrovbvph4/nuqfloKHlnpLmnLrns7vnu58g5piT5a6J6KOFIOaYk+S9v+eUqCDmlK/mjIFSRFAgU1NIIFZOQyBUZWxuZXQgS3ViZXJuZXRlc+WNj+iuriDlvq7kv6HlsI/nqIvluo8g5pel5Y6G57uE5Lu2IOWfuuS6jiB1ZWRpdG9y55qE5pu0546w5Luj5YyW55qE5a+M5paH5pys57yW6L6R5ZmoIOaUr+aMgUhUVFBT5Z+65LqOSmF2YVNjcmlwdCBSZWFjdCBWdWUy55qE5rWB56iL5Zu+57uE5Lu2IOmHh+eUqFNwcmluZyBNeUJhdGlzIFNoaXJv5qGG5p62IOW8gOWPkeeahOS4gOWll+adg+mZkOezu+e7nyDmnoHkvY7pl6jmp5sg5ou/5p2l5Y2z55SoIOiuvuiuoeS5i+WInSDlsLHpnZ7luLjms6jph43lronlhajmgKcg5Li65LyB5Lia57O757uf5L+d6am+5oqk6IiqIOiuqeS4gOWIh+mDveWPmOW+l+WmguatpOeugOWNlSBRUee+pCAzMjQ3OCAyIDQgMTQ1Nzk5OTUyIOS4gOS4quWJjeerr+eahOWNmuWuoiDmmKXmnb7lrqLmnI0g5aSa5rig6YGT5pm66IO95a6i5pyN57O757ufIOW8gOa6kOWuouacjeezu+e7nyDmnLrlmajkurrlrqLmnI3kuIDkuKrlt6XkvZzmtYHlubPlj7DlsI/nqIvluo8g5bCP5ri45oiP5Lul5Y+KIFdlYiDpgJrnlKggQ2FudmFzIOa4suafk+W8leaTjiDlnKjnur/lt6Xlhbfnp5jnsY0g5Li65Zyo57q/5bel5YW35YaZ5LiA5pys5LyY6LSo6K+05piO5LmmIOiuqeWcqOe6v+W3peWFt+mAoOemj+S6uuexuyDliY3nq6/lhoXlj4Ig5pyJ5YWz5LqOSmF2YVNjcmlwdCDnvJbnqIvojIPlvI8g6K6+6K6h5qih5byPIOi9r+S7tuW8gOWPkeeahOiJuuacr+etieWkp+WJjeerr+iMg+eVtOWGheeahOefpeivhuWIhuS6qyDml6jlnKjluK7liqnliY3nq6/lt6XnqIvluIjku6zlpK/lrp7mioDmnK/ln7rnoYDku6XpgJrov4fkuIDnur/kupLogZTnvZHkvIHkuJrmioDmnK/pnaLor5Ug77iPIHZDYXJkcyDkuK3lm73pu4TpobUg5LyY5YyWIGlPUyBBbmRyb2lkIOadpeeUtSDkv6Hmga/nlYzpnaLkvZPpqozlkITlubPlj7DnmoTliIbmtYHop4TliJkg5aSN5YaZ6KeE5YiZ5Y+K6Ieq5Yqo5YyW6ISa5pysIOWfuuS6jnZ1ZTIg55qE5a6e5pe26IGK5aSp6aG555uuIOWbvueJh+WJquijgeS4iuS8oOe7hOS7tiDnrYnnrJTorrDlv6vpgJ/liIbkuqsgR29vZ2xlRHJpdmUgT25lRHJpdmUg5q+P5pel5pe25oqlIOS7peWJjeerr+aKgOacr+S9k+ezu+S4uuS4u+imgeWIhuS6q+ivvumimCDmoLnmja4g5paH56ugIOW3peWFtyDmlrDpl7sg6KeG6aKR5Yeg5aSn5p2/5Z2X5L2c5Li65Li76KaB5YiG57G7IOS4gOasvumrmOaViCDpq5jmgKfog73nmoTluKfliqjnlLvnlJ/miJDlt6Xlhbcg5YGc5q2i57u05oqkIOS4gOS4quWcqOe6v+mfs+S5kOaSreaUvuWZqCDku4UgVUkg5peg5Yqf6IO9IOWwj+eoi+W6j+WvjOaWh+acrOe7hOS7tiDmlK/mjIHmuLLmn5PlkoznvJbovpEgaHRtbCDmlK/mjIHlnKjlvq7kv6EgUVEg55m+5bqmIOaUr+S7mOWunSDlpLTmnaHlkowgdW5pIGFwcCDlubPlj7Dkvb/nlKjln7rkuo4gZWxlY3Ryb24gdnVlIOW8gOWPkeeahOmfs+S5kOaSreaUvuWZqCDnlYzpnaLmqKHku79RUemfs+S5kCDmioDmnK/moIjmrKLov45zdGFyd2V1aSDmmK/lnKh3ZXVp5ZKMemVwdG/ln7rnoYDkuIrlvIDlj5HnmoTlop7lvLpVSee7hOS7tiDnm67liY3liIbkuLrooajljZUg5Z+656GAIOe7hOS7tiBqc+aPkuS7tuWbm+Wkp+exuyDlhbHorqHnmb7kvZnpobnlip/og70g5piv5pyA5YWo55qEd2V1aeagt+W8j+WQjOatpeWSjOabtOaWsOWkp+S9rOiEmuacrOW6kyDmm7TmlrDmh5LkurrphY3nva7ohb7orq/kupHljbPml7bpgJrkv6EgSU0g5pyN5YqhIOWbveWGheS4i+i9vemVnOWDjyDln7rkuo4gVnVlIOeahOWwj+eoi+W6j+W8gOWPkeahhuaetlJlYWN0IDE2IDjmiZPpgKDnsr7nvo7pn7PkuZBXZWJBcHBXS+ezu+WIl+W8gOWPkeahhuaetiBWMeiHs1Y1IEphdmHlvIDmupDkvIHkuJrnuqflvIDlj5HmoYbmnrYg5Y2V5bqU55SoIOW+ruacjeWKoSDliIbluIPlvI8g77iP5Lit5Zu9IOecgeW4guWMuiDkuInnuqfogZTliqgg5Zyw5Z2A6YCJ5oup5ZmoIOW+ruS/oeWwj+eoi+W6jzJk5Yqo55S75bqTIOWIhuW4g+W8jyBSZWRpc+e8k+WtmCBTaGlyb+adg+mZkOeuoeeQhiBTcHJpbmcgU2Vzc2lvbuWNleeCueeZu+W9lSBRdWFydHrliIbluIPlvI/pm4bnvqTosIPluqYgUmVzdGZ1bOacjeWKoSBRUSDlvq7kv6HnmbvlvZUgQXBwIHRva2Vu55m75b2VIOW+ruS/oSDmlK/ku5jlrp3mlK/ku5gg5pel5pyf6L2s5o2iIOaVsOaNruexu+Wei+i9rOaNoiDluo/liJfljJYg5rGJ5a2X6L2s5ou86Z+zIOi6q+S7veivgeWPt+eggemqjOivgSDmlbDlrZfovazkurrmsJHluIEg5Y+R6YCB55+t5L+hIOWPkemAgemCruS7tiDliqDlr4bop6Plr4Yg5Zu+54mH5aSE55CGIGV4Y2Vs5a+85YWl5a+85Ye6IEZUUCBTRlRQIGZhc3RERlPkuIrkvKDkuIvovb0g5LqM57u056CBIFhNTOivu+WGmSDpq5jnsr7luqborqHnrpcg57O757uf6YWN572u5bel5YW357G7562J562JIEVkdVNvaG8g572R57uc6K++5aCC5piv55Sx5p2t5bee6ZiU55+l572R57uc56eR5oqA5pyJ6ZmQ5YWs5Y+456CU5Y+R55qE5byA5rqQ572R5qCh57O757ufIEVkdVNvaG8g5YyF5ZCr5LqG5Zyo57q/5pWZ5a2mIOaLm+eUn+WSjOeuoeeQhuetieWujOaVtOWKn+iDvSDorqnmlZnogrLmnLrmnoTlj6/ku6Xpm7bpl6jmp5vlu7rnq4vnvZHmoKEg5oiQ5Yqf6L2s5Z6L5Zyo57q/5pWZ6IKyIEVkdVNvaG8g5Lmf5Y+v5L2c5Li65LyB5Lia5YaF6K6t5bmz5Y+wIOW4ruWKqeS8geS4muWunueOsOS6uuaJjeWfueWFuyDoh6rnlKjnmoTkuIDkupvkubHkuIPlhavns58g5rK554y06ISa5pysIOS4uuWImuWImuWtpuS5oHBocOivreiogOS7peWPindlYue9keermeW8gOWPkeaVtOeQhueahOS4gOWll+i1hOa6kCDmnInop4bpopEg5a6e5oiY5Luj56CBIOWtpuS5oOi3r+W+hOetiSDkvJrmjIHnu63mm7TmlrAgVGhpcyBpcyBhIGdvaW5kZXggdGhlbWUg5LiA5LiqZ29pbmRleOeahOaJqeWxleS4u+mimCBOdW1QeeWumOaWueS4reaWh+aWh+ahoyDlrozmlbTniYgg5pCt5bu656e75Yqo56uv5byA5Y+RIOWfuuS6jumAgumFjeaWueahiCBheGlvc+WwgeijhSDmnoTlu7rmiYvmnLrnq6/mqKHmnb/ohJrmiYvmnrYg5ZCO5Y+w566h55CGIOiEmuaJi+aetuaOpeWPoyDku47nroDljZXlvIDlp4sgUGhhbEFwaeeugOensM+A5qGG5p62IOS4gOS4qui9u+mHj+e6p1BIUOW8gOa6kOaOpeWPo+ahhuaetiDkuJPms6jkuo7mjqXlj6PmnI3liqHlvIDlj5Eg5YmN56uv54m55pWI5a2Y5qGjQ2hyb21l5rWP6KeI5ZmoIOaKoui0rSDnp5LmnYDmj5Lku7Yg56eS5p2A5Yqp5omLIOWumuaXtuiHquWKqOeCueWHu+S6keWtmOWCqOeuoeeQhuWuouaIt+erryDmlK/mjIHkuIPniZvkupEg6IW+6K6v5LqRIOmdkuS6kSDpmL/ph4zkupEg5Y+I5ouN5LqRIOS6mumprOmAilMzIOS6rOS4nOS6kSDku7/mlofku7blpLnnrqHnkIYg5Zu+54mH6aKE6KeIIOaLluaLveS4iuS8oCDmlofku7blpLnkuIrkvKAg5ZCM5q2lIOaJuemHj+WvvOWHulVSTOetieWKn+iDvWZvbnQgY2FycmllcuaYr+S4gOS4quWKn+iDveW8uuWkp+eahOWtl+S9k+aTjeS9nOW6kyDkvb/nlKjlroPkvaDlj6/ku6Xpmo/lv4PmiYDmrLLnmoTmk43kvZzlrZfkvZMg6K6p5L2g5Y+v5Lul5Zyoc3Zn55qE57u05bqm5pS56YCg5a2X5L2T55qE5bGV546w5b2i54q2IENSTuaYr0N0cmlwIFJlYWN0IE5hdGl2ZeeugOensCDnlLHmkLrnqIvml6Dnur/lubPlj7DnoJTlj5Hlm6LpmJ/ln7rkuo5SZWFjdCBOYXRpdmXmoYbmnrbkvJjljJYg5a6a5Yi25oiQ56iz5a6a5oCn5ZKM5oCn6IO95pu05L2zIOS5n+abtOmAguWQiOS4muWKoeWcuuaZr+eahOi3qOW5s+WPsOW8gOWPkeahhuaetiDmsrnnjLTohJrmnKzpobXpnaLmta7nqpflub/lkYrlrozlhajov4fmu6Tlh4DljJYg5Zu95pyN5pyA5by65pyA5YWo5pyA5pawQ1NETuiEmuacrOW+ruS/oeWwj+eoi+W6j+WNs+aXtumAmuiur+aooeadvyDkvb/nlKhXZWJTb2NrZXTpgJrkv6HlsI/nqIvluo/lj43nvJbor5Eg5pSv5oyB5YiG5YyFIOKAnOaDs+WtpuWQl+KAneS4quS6uuefpeivhueuoeeQhuS4juiHquWqkuS9k+iQpemUgOW3peWFtyDotoXlpJrnu4/lhbggQ2FudmFzIOWunuS+iyDliqjmgIHnprvlrZDog4zmma8g54Kr5b2p5bCP55CDIOi0quWQg+ibhyDlnablhYvlpKfmiJgg5piv55S35Lq65bCx5LiLMSDlsYIg5b+D5b2i5paH5a2X562JIFZ1ZSBVRWRpdG9yIHYgbW9kZWzlj4zlkJHnu5HlrpogSFFDaGFydCBINSDlvq7kv6HlsI/nqIvluo8g5rKq5rexIOa4r+iCoSDmlbDlrZfotKfluIEg5pyf6LSnIOe+juiCoSBL57q/5Zu+IGtsaW5lIOi1sOWKv+WbviDnvKnmlL4g5ouW5ou9IOWNgeWtl+WFieaghyDnlLvlm77lt6Xlhbcg5oiq5Zu+IOetueeggeWbviDliIbmnpDlrrbor63ms5Ug6YCa6L6+5L+h6K+t5rOVIOm6puivreazlSDnrKwz5pa55pWw5o2u5pu/5o2i5o6l5Y+j5Z+65LqOa29hMueahOagh+WHhuWJjeWQjuerr+WIhuemu+ahhuaetiDkuIDmrL7kvIHkuJrkv6Hmga/ljJblvIDlj5Hln7rnoYDlubPlj7Ag5ouf6ZuG5oiQT0Eg5Yqe5YWs6Ieq5Yqo5YyWIENNUyDlhoXlrrnnrqHnkIbns7vnu58g562J5LyB5Lia57O757uf55qE6YCa55So5Lia5Yqh5Yqf6IO9IEplZVBsYXRmb3Jt6aG555uu5piv5LiA5qy+5LulU3ByaW5nQm9vdOS4uuaguOW/g+ahhuaetiDpm4ZPUk3moYbmnrZNeWJhdGlzIFdlYuWxguahhuaetlNwcmluZ01WQ+WSjOWkmuenjeW8gOa6kOe7hOS7tuahhuaetuiAjOaIkOeahOS4gOasvumAmueUqOWfuuehgOW5s+WPsCDku6PnoIHlt7Lnu4/mjZDotaDnu5nlvIDmupDkuK3lm73npL7ljLrln7rkuo5pbmNlcHRpb27nmoToh6rliqjljJZTUUzmk43kvZzlubPlj7Ag5pSv5oyBU1FM5omn6KGMIExEQVDorqTor4Eg5Y+R6YKu5Lu2IE9TQyBTUUzmn6Xor6IgU1FM5LyY5YyW5bu66K6uIOadg+mZkOeuoeeQhuetieWKn+iDvSDmlK/mjIFkb2NrZXLplZzlg4/mmK/kuIDmrL7kuJPpl6jpnaLlkJHkuKrkurog5Zui6Zif5ZKM5bCP5Z6L57uE57uH55qE56eB5pyJ572R55uY57O757ufIOi9u+mHjyDlvIDmupAg5a6M5ZaEIOaXoOiuuuaYr+WcqOWutuW6rSDlrabmoKHov5jmmK/lnKjlip7lhazlrqQg5oKo6YO96IO956uL5Yi75byA5aeL5L2/55So5a6DIOS6huino+abtOWkmuivt+iuv+mXruWumOaWuee9keermSBOb2RlIGpzIEFQSSDkuK3mlofmlofmoaNkdWJib+acjeWKoeeuoeeQhuS7peWPiuebkeaOp+ezu+e7n+aLr+aVkULnq5nnmoTlvLnluZXkvZPpqowg5a+55oqX5YGH5raI5oGv57O75YiX6aG555uu5LmL5LiAIOaIquWxjyDlrp7plKTvvJ/nm7jkv6HkvaDlsLHovpPkuoYg4oCd56qB56C05oCn4oCc5pu05pawIOaUr+aMgeS/ruaUueS7u+S9lee9keermSDvuI/kuIDkuKrnroDmtIEg5LyY6ZuF5LiU6auY5pWI55qEIEh1Z28g5Li76aKYVnVlIGpzIOekuuS+i+mhueebriDnroDmmJPnlZnoqIDmnb8g5pys6aG555uu5oul5pyJ5a6M5ZaE55qE5paH5qGj6K+05piO5LiO5rOo6YeKIOiuqeaCqOW/q+mAn+S4iuaJiyBWdWUganMg5byA5Y+RPyBWdWUgVmFsaWRhdG9yPyBWdWV4P+acgOS9s+Wunui3teWfuuS6jiBOb2RlIGpzIEtvYTIg5a6e5oiY5byA5Y+R55qE5LiA5aWX5a6M5pW055qE5Y2a5a6i6aG555uu572R56uZIOeUqCBSZWFjdCDnvJblhpnnmoTln7rkuo5UYXJvIER2YeaehOW7uueahOmAgumFjeS4jeWQjOerryDlvq7kv6Eg55m+5bqmIOaUr+S7mOWuneWwj+eoi+W6jyBINSBSZWFjdCBOYXRpdmUg562JIOeahOaXtuijheiho+apseS/oeaBr+azhOa8j+ebkeaOp+ezu+e7nyDkvKroo4UxMTXmtY/op4jlmajlubLniIbliY3nq68g5LiA572R5omT5bC95YmN56uv6Z2i6K+VIOWtpuS5oOi3r+W+hCDkvJjnp4Dlpb3mlofnrYnlkITnsbvlhoXlrrkg5biu5Yqp5aSn5a625LiA5bm05YaF5ou/5Yiw5pyf5pyb55qEIG9mZmVyIOWJjeerr+aAp+iDveebkeaOp+ezu+e7nyDmtojmga/pmJ/liJcg6auY5Y+v55SoIOmbhue+pOetieebuOWFs+aetuaehFNwcmluZ0Jvb3QgdjLpobnnm67mmK/liqrlipvmiZPpgKBzcHJpbmdib2905qGG5p6255qE5p6B6Ie057uG6IW755qE6ISa5omL5p62IOWMheaLrOS4gOWll+a8guS6rueahOWJjeWPsCDml6Dlhbbku5bmnYLkuIPmnYLlhavnmoTlip/og70g5Y6f55Sf57qv5YeAIOS4reaWh+aWh+acrOagh+azqOW3peWFtyDmnIDlhajmnIDmlrDkuK3lm70g55yBIOW4giDljLrljr8g5Lmh6ZWH6KGX6YGTIGpzb24gY3N2IHNxbOaVsOaNriDkuIDmrL7ovbvlt6fnmoTmuJDov5vlvI/lvq7kv6HlsI/nqIvluo/moYbmnrYg5YWo572RIDEgdyDpmIXor7vph4/nmoTov5vpmLbliY3nq6/mioDmnK/ljZrlrqLku5PlupMgVnVlIOa6kOeggeino+aekCBSZWFjdCDmt7Hluqblrp7ot7UgVHlwZVNjcmlwdCDov5vpmLboibrmnK8g5bel56iL5YyWIOaAp+iDveS8mOWMluWunui3tSDlrozmlbTlvIDmupAgSmF2YeW/q+mAn+W8gOWPkeW5s+WPsCDln7rkuo5TcHJpbmcgU3ByaW5nTVZDIE15YmF0aXPmnrbmnoQgTVN0b3Jl5o+Q5L6b5pu05aSa5aW955So55qE5o+S5Lu25LiO5qih5p2/IOaWh+eroCDllYbln44g5b6u5L+hIOiuuuWdmyDkvJrlkZgg6K+E6K66IOaUr+S7mCDnp6/liIYg5bel5L2c5rWBIOS7u+WKoeiwg+W6puetiSDlkIzml7bmj5DkvpvkuIrnmb7lpZflhY3otLnmqKHmnb/ku7vmhI/pgInmi6kg5Lu35YC85rqQ6Ieq5YiG5LqrIOmTremjnuezu+e7n+S4jeS7heS4gOWll+eugOWNleWlveeUqOeahOW8gOa6kOezu+e7nyDmm7TmmK/kuIDmlbTlpZfkvJjotKjnmoTlvIDmupDnlJ/mgIHlhoXlrrnkvZPns7sg6ZOt6aOe55qE5L2/5ZG95bCx5piv6ZmN5L2O5byA5Y+R5oiQ5pys5o+Q6auY5byA5Y+R5pWI546HIOaPkOS+m+WFqOaWueS9jeeahOS8geS4mue6p+W8gOWPkeino+WGs+aWueahiCDmr4/mnIgyOOWumuacn+abtOaWsOeJiOacrFdlSGFsbyDnroDnuqbpo44g55qE5b6u5L+h5bCP56iL5bqP54mI5Y2a5a6iIOWfuuS6jiB2dWUyIHZ1ZXgg5p6E5bu65LiA5Liq5YW35pyJIDQ1IOS4qumhtemdoueahOWkp+Wei+WNlemhtemdouW6lOeUqOWfuuS6jlZ1ZTMgRWxlbWVudCBQbHVzIOeahOWQjuWPsOeuoeeQhuezu+e7n+ino+WGs+aWueahiOWfuuS6jiB2dWUgZWxlbWVudCB1aSDnmoTlkI7lj7DnrqHnkIbns7vnu5/pspzkuq7nmoTpq5jppbHlkozoibLlvakg5LiT5rOo6KeG6KeJ55qE5bCP56iL5bqP57uE5Lu25bqTIO+4jyDot6jlubPlj7DmoYzpnaLnq6/op4bpopHotYTmupDmkq3mlL7lmagg566A5rSB5peg5bm/5ZGKIOWFjei0uemrmOminOWAvCDlkI7lj7DnrqHnkIbkuLvnur/niYjmnKzln7rkuo7kuInogIXlubbooYzlvIDlj5Hnu7TmiqQg5ZCM5pe25pSv5oyB55S16ISRIOaJi+acuiDlubPmnb8g5YiH5o2i5YiG5pSv5p+l55yL5LiN5ZCM55qEdnVl54mI5pysIGVsZW1lbnQgcGx1c+eJiOacrOW3suWPkeW4gyB2dWUzIHZ1ZTMgdnVlIHZ1ZTMgeCB2dWUganMg56iL5bqP5peg5Zu955WMIOS9hueoi+W6j+WRmOacieWbveeVjCDkuK3lm73lm73lrrblsIrkuKXkuI3lrrnmjJHooYUg5aaC5p6c5oKo5Zyo54m55q6K5pe25pyfIG1hbGwgYWRtaW4gd2Vi5piv5LiA5Liq55S15ZWG5ZCO5Y+w566h55CG57O757uf55qE5YmN56uv6aG555uuIOWfuuS6jlZ1ZSBFbGVtZW505a6e546wIOS4u+imgeWMheaLrOWVhuWTgeeuoeeQhiDorqLljZXnrqHnkIYg5Lya5ZGY566h55CGIOS/g+mUgOeuoeeQhiDov5DokKXnrqHnkIYg5YaF5a65566h55CGIOe7n+iuoeaKpeihqCDotKLliqHnrqHnkIYg5p2D6ZmQ566h55CGIOiuvue9ruetieWKn+iDvSDkuIDmrL7lrozlloTnmoTlronlhajor4TkvLDlt6Xlhbcg5pSv5oyB5bi46KeBIHdlYiDlronlhajpl67popjmiavmj4/lkozoh6rlrprkuYkgcG9jIOS9v+eUqOS5i+WJjeWKoeW/heWFiOmYheivu+aWh+aho1Z1ZeaVsOaNruWPr+inhuWMlue7hOS7tuW6kyDnsbvkvLzpmL/ph4xEYXRhViDlpKflsY/mlbDmja7lsZXnpLog5o+Q5L6bU1ZH55qE6L655qGG5Y+K6KOF6aWwIOWbvuihqCDmsLTkvY3lm74g6aOe57q/5Zu+562J57uE5Lu2IOeugOWNleaYk+eUqCDplb/mnJ/mm7TmlrAgUmVhY3TniYjlt7Llj5HluIMgVUnooajljZXorr7orqHlj4rku6PnoIHnlJ/miJDlmajln7rkuo5WdWXnmoTlj6/op4bljJbooajljZXorr7orqHlmagg6K6p6KGo5Y2V5byA5Y+R566A5Y2V6ICM6auY5pWIIOWfuuS6jnZ1ZeeahOmrmOaJqeWxleWcqOe6v+e9kemhteWItuS9nOW5s+WPsCDlj6/oh6rlrprkuYnnu4Tku7Yg5Y+v5re75Yqg6ISa5pysIOWPr+aVsOaNrue7n+iuoSB2dWXlkI7lj7DnrqHnkIbmoYbmnrYg57K+6Ie055qE5LiL5ouJ5Yi35paw5ZKM5LiK5ouJ5Yqg6L29IGpz5qGG5p62IOaUr+aMgXZ1ZSDlroznvo7ov5DooYzkuo7np7vliqjnq6/lkozkuLvmtYFQQ+a1j+iniOWZqCDln7rkuo52dWUyIHZ1ZXggZWxlbWVudCB1aeWQjuWPsOeuoeeQhuezu+e7nyBWdWUganPpq5jku7/ppb/kuobkuYjlpJbljZZBcHDor77nqIvmupDnoIEgY29kaW5nIGltb29jIGNvbSBjbGFzcyA3NCBodG1s5Lqs5Lic6aOO5qC856e75Yqo56uvIFZ1ZTIgVnVlMyDnu4Tku7blupNlbGFkbWlu5YmN56uv5rqQ56CBIOmhueebruWfuuS6jueahOWJjeWQjuerr+WIhuemu+WQjuWPsOeuoeeQhuezu+e7nyDmnYPpmZDmjqfliLbph4fnlKggUkJBQyDoj5zljZXliqjmgIHot6/nlLHotYTmupDph4fpm4bnq5nlnKjnur/mkq3mlL51VmlldyBVSSDmmK91bmkgYXBw55Sf5oCB5pyA5LyY56eA55qEVUnmoYbmnrYg5YWo6Z2i55qE57uE5Lu25ZKM5L6/5o2355qE5bel5YW35Lya6K6p5oKo5L+h5omL5ouI5p2lIOWmgumxvOW+l+awtFZ1ZTIg5YWo5a625qG25Lu/IOW+ruS/oUFwcCDpobnnm64g5pSv5oyB5aSa5Lq65Zyo57q/6IGK5aSp5ZKM5py65Zmo5Lq66IGK5aSp5YmN56uvdnVlIOWQjuerr2tvYSDlhajmoIjlvI/lvIDlj5FiaWxpYmlsaemmlumhtSBBIG1hZ2ljYWwgdnVlIGFkbWluIOiusOW+l3N0YXLkupLogZTnvZHlpKfljoLlhoXmjqjlj4rlpKfljoLpnaLnu4/mlbTnkIYg5bm25LiU5q+P5aSp5LiA6YGT6Z2i6K+V6aKY5o6o6YCBIOavj+WkqeS6lOWIhumSnyDljYrlubTlpKfljoLkuK0gVnVlMyDlhajlrrbmobYgVmFudCDmkK3lu7rlpKflnovljZXpobXpnaLllYbln47pobnnm64g5paw6JyC5ZWG5Z+OIFZ1ZTMg54mI5pysIOaKgOacr+agiOS4uuWfuuS6jlZ1ZeW8gOWPkeeahFhNYWxs5ZWG5Z+O5YmN5Y+w6aG16Z2iIFBD56uvIFZ1ZeWFqOWutuahtiBWYW50IOaQreW7uuWkp+Wei+WNlemhtemdoueUteWVhumhueebriBkZGJ1eSA3IG9yYW5nZSBjbuWJjeWQjuerr+WIhuemu+adg+mZkOeuoeeQhuezu+e7nyDnsr7lipvmnInpmZAg5YGc5q2i57u05oqkIOeUqCBWdWUganMg5byA5Y+R55qE6Leo5LiJ56uv5bqU55SoUHJvdG90eXBpbmcgVG9vbCBGb3IgVnVlIERldnMg6YCC55So5LqOVnVl55qE5Y6f5Z6L5bel5YW35a6e5oiY5ZWG5Z+OIOWfuuS6jlZ1ZTIg6auY5Lu/5b6u5L+hQXBw55qE5Y2V6aG15bqU55SoZWxlY3Ryb27ot6jlubPlj7Dpn7PkuZDmkq3mlL7lmagg5Y+v5pCc572R5piT5LqRIFFR6Z+z5LmQIOiZvuexs+mfs+S5kCDmlK/mjIFRUSDlvq7ljZogR2l0aHVi55m75b2VIOS6keatjOWNlSDmlK/mjIHkuIDplK7lr7zlhaXpn7PkuZDlubPlj7DmrYzljZVUaG9yVUnnu4Tku7blupMg6L276YePIOeugOa0geeahOenu+WKqOerr+e7hOS7tuW6kyDnu4Tku7bmlofmoaPlnLDlnYAgdGhvcnVpIGNuIGRvYyDmnIDov5Hmm7TmlrDml7bpl7QgMiAyMSA1IDI4dW5pIGFwcOahhuaetua8lOekuuekuuS+iyBFbGVjdHJvbiBWdWUg5Lu/572R5piT5LqR6Z+z5LmQd2luZG93c+WuouaIt+erryDln7rkuo4gVnVlMiBWdWUgQ0xJMyDnmoTpq5jku7/nvZHmmJPkupEgbWFjIOWuouaIt+err+aSreaUvuWZqCBQQyBPbmxpbmUgTXVzaWMgUGxheWVyR2l0SHViIOazhOmcsuebkeaOp+ezu+e7n3BlYXIg5qKo5a2QIOi9u+mHj+e6p+eahOWcqOe6v+mhueebriDku7vliqHljY/kvZzns7vnu58g6L+c56iL5Yqe5YWs5Y2P5L2c6Ieq6YCJ5Z+66YeR5Yqp5omL5piv5LiA5qy+Q2hyb21l5omp5bGVIOeUqOadpeW/q+mAn+iOt+WPluWFs+azqOWfuumHkeeahOWunuaXtuaVsOaNriDmn6XnnIvoh6rpgInln7rph5HnmoTlrp7ml7bkvLDlgLzmg4XlhrXmlK/mjIEgbWFya2Rvd24g5riy5p+T55qE5Y2a5a6i5YmN5Y+w5bGV56S6VnVlIOmfs+S5kOaQnOe0oiDmkq3mlL4gRGVtbyDkuIDkuKrln7rkuo4gdnVlMiB2dWUzIOeahCDlpKfovaznm5gg5Lmd5a6r5qC8IOaKveWlluaPkuS7tuWlluWTgSDmloflrZcg5Zu+54mHIOminOiJsiDmjInpkq7lnYflj6/phY3nva4g5pSv5oyB5ZCM5q2lIOW8guatpeaKveWlliDmpoLnjofliY0g5ZCO56uv5Y+v5o6nIOiHquWKqOagueaNriBkcHIg6LCD5pW05riF5pmw5bqm6YCC6YWN56e75Yqo56uvIOWfuuS6jiBWdWUg55qE5Zyo57q/6Z+z5LmQ5pKt5pS+5ZmoIFBDIE9ubGluZSBtdXNpYyBwbGF5ZXLnvo7lm6Lppb/kuoblkJflpJbljZbnuqLljIXlpJbljZbkvJjmg6DliLgg5YWI6aKG57qi5YyF5YaN5LiL5Y2VIOWkluWNlue6ouWMheS8mOaDoOWIuCBjcHPliIbmiJAg5Yir5Lq66aKG57qi5YyF5LiL5Y2VIOS9oOaLv+S9o+mHkSDorqjorrrlpoLkvZXmnoTlu7rkuIDlpZflj6/pnaDnmoTlpKflnovliIbluIPlvI/ns7vnu5/nlKggdnVlIOWGmeWwj+eoi+W6jyDln7rkuo4gbXB2dWUg5qGG5p626YeN5YaZIHdldWkg5Z+65LqOVnVl5qGG5p625p6E5bu655qEZ2l0aHVi5pWw5o2u5Y+v6KeG5YyW5bmz5Y+w5L2/55SoR2l0SHViIEFQSSDmkK3lu7rkuIDkuKrlj6/liqjmgIHlj5HluIPmlofnq6DnmoTljZrlrqLlj6/op4bljJbmi5bmi73nu4Tku7blupMgREVNT+WfuuS6juW8gOa6kOe7hOS7tiBJbmNlcHRpb24gU1FMQWR2aXNvciBTT0FSIOeahFNRTOWuoeaguCBTUUzkvJjljJbnmoRXZWLlubPlj7DmmL7npLrlvZPliY3nvZHnq5nnmoTmiYDmnInlj6/nlKhUYW1wZXJtb25rZXnohJrmnKwg5LiT5rOoV2Vi5LiO566X5rOV5peg57yd5rua5YqoY29tcG9uZW5057K+6YCa5Lul5aSq5Z2KIOS4reaWh+eJiCDnvZHpobXmqKHmi5/moYzpnaLln7rkuo7nmoTlpJrmqKHlnZfliY3lkI7nq6/liIbnprvnmoTljZrlrqLpobnnm67nvZHmmJPkupHpn7PkuZAgUVHpn7PkuZAg5ZKq5ZKV6Z+z5LmQIOesrOS4ieaWuSB3ZWLnq68g5Y+v5pKt5pS+IHZpcCDkuIvmnrbmrYzmm7Ig5Z+65LqO5p2D6ZmQ566h55CG55qE5ZCO5Y+w566h55CG57O757uf5Z+65LqOIE5vZGUganMg55qE5byA5rqQ5Liq5Lq65Y2a5a6i57O757ufIOmHh+eUqCBOdXh0IFZ1ZSBUeXBlU2NyaXB0IOaKgOacr+agiCDkuIDmrL7nroDmtIHpq5jmlYjnmoRWdWVQcmVzc+efpeivhueuoeeQhiDljZrlrqIgYmxvZyDkuLvpopjln7rkuo51bmkgYXBw55qEdWnmoYbmnrbln7rkuo5WdWUgVnVleCBpVmlld+eahOeUteWtkOWVhuWfjue9keermSBWdWUyIOWFqOWutuahtiBWYW50IOaQreW7uuWkp+Wei+WNlemhtemdouWVhuWfjumhueebriDmlrDonILllYbln47liY3lkI7nq6/liIbnprvniYjmnKwg5YmN56uvVnVl6aG555uu5rqQ56CB6YW354uXIO+4jyDmnoHlrqLnjL/moqblr7zoiKog54us56uL5byA5Y+R6ICF55qE5a+86Iiq56uZIFZ1ZSBTcHJpbmdCb290IE15QmF0aXMg6Z+z5LmQ572R56uZ5Z+65LqOIFJhZ2VGcmFtZTIg55qE5LiA5qy+5YWN6LS55byA5rqQ55qE5Z+656GA5ZWG5Z+O6ZSA5ZSu5Yqf6IO955qE5byA5rqQ5b6u5ZWG5Z+OIOWfuuS6jnZ1ZTIg55qE572R5piT5LqR6Z+z5LmQ5pKt5pS+5ZmoIGFwaeadpeiHquS6jk5ldGVhc2VDbG91ZE11c2ljQXBpIHYyIOS4uuacgOaWsOeJiOacrOe8luWGmeeahOS4gOWll+WQjuWPsOeuoeeQhuezu+e7n+WFqOagiOW8gOWPkeeOi+iAheiNo+iAgOaJi+acuuerr+WumOe9keWSjOeuoeeQhuWQjuWPsOWfuuS6jiBWdWUzIHggVHlwZVNjcmlwdCDnmoTlnKjnur/mvJTnpLrmlofnqL/lupTnlKgg5a6e546wUFBU5bm754Gv54mH55qE5Zyo57q/57yW6L6RIOa8lOekuiDln7rkuo52dWUyIOeUn+aAgeeahOWQjuWPsOeuoeeQhuezu+e7n+aooeadv+W8gOWPkeeahOWQjuWPsOeuoeeQhuezu+e7nyBWY2hhdCDku47lpLTliLDohJog5pK45LiA5Liq56S+5Lqk6IGK5aSp57O757ufIHZ1ZSBub2RlIG1vbmdvZGIgaDXnvJbovpHlmajnsbvkvLxtYWthIOaYk+S8geengCDotKblj7cg5a+G56CBIGFkbWluOTk2IOWFrOWPuOWxleekuiDorqjorrpWdWUgU3ByaW5nIGJvb3TliY3lkI7nq6/liIbnprvpobnnm64gd2ggd2ViIHdoIHNlcnZlcueahOWNh+e6p+eJiCDln7rkuo5lbGVtZW50IHVp55qE5pWw5o2u6amx5Yqo6KGo5Y2V57uE5Lu25Z+65LqOIEdpdEh1YiBBUEkg5byA5Y+R55qE5Zu+5bqK56We5ZmoIOWbvueJh+WklumTvuS9v+eUqCBqc0RlbGl2ciDov5vooYwgQ0ROIOWKoOmAnyDlhY3kuIvovb0g5YWN5a6J6KOFIOaJk+W8gOe9keermeWNs+WPr+ebtOaOpeS9v+eUqCDlhY3otLkg56iz5a6aIOmrmOaViCDvuI8gVnVl5YidIOS4ree6p+mhueebriBDbm9kZUpT56S+5Yy66YeN5p6E6aKE6KeIIERFTU8g5Z+65LqOdnVlMuWFqOWutuahtuWunueOsOeahCDku7/np7vliqjnq69RUeWfuuS6jlZ1ZSBFY2hhcnRzIOaehOW7uueahOaVsOaNruWPr+inhuWMluW5s+WPsCDphbfngqvlpKflsY/lsZXnpLrmqKHmnb/lkoznu4Tku7blupMg5oyB57ut5pu05paw5ZCE6KGM5ZCE5Lia5a6e55So5qih5p2/5ZKM54Kr6YW35bCP57uE5Lu2IOWfuuS6jlNwcmluZyBCb29055qE5Zyo57q/6ICD6K+V57O757ufIOmihOiniOWcsOWdgCAxMjkgMjExIDg4IDE5MSDotKbmiLfliIbliKvmmK9hZG1pbiB0ZWFjaGVyIHN0dWRlbnQg5a+G56CB5pivYWRtaW4xMjMgNnBhbiA255uY5bCP55m9576KIOesrOS6jOeJiCB2dWUzIGFudGQgdHlwZXNjcmlwdCBvbiBib25lIGFuZCBrbmlmZSDln7rkuo5WdWUg5YWo5a625qG2IDIgeCDliLbkvZznmoTnvo7lm6LlpJbljZZBUFAg5pys6aG555uu5piv5LiA5qy+5Z+65LqOIEF2dWUg55qE6KGo5Y2V6K6+6K6h5ZmoIOaLluaLveW8j+aTjeS9nOiuqeS9oOW/q+mAn+aehOW7uuS4gOS4quihqOWNlSDkuIDliLvnpL7ljLrliY3nq6/mupDnoIHln7rkuo5WdWUgaVZpZXcgQWRtaW7lvIDlj5HnmoRYQm9vdOWJjeWQjuerr+WIhuemu+W8gOaUvuW5s+WPsOWJjeerryDmnYPpmZDlj6/mjqfliLboh7PmjInpkq7mmL7npLog5Yqo5oCB6Lev55Sx5p2D6ZmQ6I+c5Y2VIOWkmuivreiogCDnroDmtIHnvo7op4Ig5YmN5ZCO56uv5YiG56a7IO+4j+S4gOS4quW8gOa6kOeahOekvuWMuueoi+W6jyDkuLTml7bmtYvor5Xnq5kgaHR0cHMgdCBteXJwZyBjbmVjaGFydHPlnLDlm75nZW9Kc29u6KGM5pS/6L6555WM5pWw5o2u55qE5a6e5pe26I635Y+W5LiO5bqU55SoIOecgeW4guWMuuWOv+Wkmue6p+iBlOWKqOS4i+mSuyDnnJ/mraPmhI/kuYnnmoTkuIvpkrvoh7Pljr/nuqcg6ZmE5pyA5pawZ2VvSnNvbuaWh+S7tuS4i+i9vSBWdWXnmoROdXh0IGpz5pyN5Yqh56uv5riy5p+T5qGG5p62IE5vZGVKU+S4uuWQjuerr+eahOWFqOagiOmhueebriBEb2NrZXLkuIDplK7pg6jnvbIg6Z2i5ZCR5bCP55m955qE5a6M576O5Y2a5a6i57O757ufdnVl54CR5biD5rWB57uE5Lu2IHZ1ZSB3YXRlcmZhbGwgZWFzeSAyIHggRWdvIOenu+WKqOerr+i0reeJqeWVhuWfjiB2dWUgdnVleCBydW90ZXIgd2VicGFjayBWdWUganMgTm9kZSBqcyBNb25nb2RiIOWJjeWQjuerr+WIhuemu+eahOS4quS6uuWNmuWuouWktOWDj+WKoOWPo+e9qeWwj+eoi+W6jyDln7rkuo51bmlhcHDkvb/nlKh2dWXlv6vpgJ/lrp7njrAg5bm/5ZGK5pyI5pS25YWlNGsg5Z+65LqOdnVlMyDnmoTnrqHnkIbnq6/mqKHmnb/mlZnkvaDlpoLkvZXmiZPpgKDoiJLpgIIg6auY5pWIIOaXtuWwmueahOWJjeerr+W8gOWPkeeOr+Wig+WfuuS6jiBGbGFzayDlkowgVnVlIGpzIOWJjeWQjuerr+WIhuemu+eahOW+ruWei+WNmuWuoumhueebriDmlK/mjIHlpJrnlKjmiLcgTWFya2Rvd27mlofnq6Ag5Zac5qyiIOaUtuiXj+aWh+eroCDnsonkuJ3lhbPms6gg55So5oi36K+E6K66IOeCuei1niDliqjmgIHpgJrnn6Ug56uZ5YaF56eB5L+hIOm7keWQjeWNlSDpgq7ku7bmlK/mjIEg566h55CG5ZCO5Y+wIOadg+mZkOeuoeeQhiBSUeS7u+WKoemYn+WIlyBFbGFzdGljc2VhcmNo5YWo5paH5pCc57SiIExpbnV4IFZQU+mDqOe9siBEb2NrZXLlrrnlmajpg6jnvbLnrYnln7rkuo4gdnVlIOWSjCBoZXl1aSDnu4Tku7blupPnmoTkuK3lkI7nq6/ns7vnu58gYWRtaW4gaGV5dWkgdG9wVnVlIOi9u+mHj+e6p+WQjuWPsOeuoeeQhuezu+e7n+WfuuehgOaooeadv3VuaSBhcHDpobnnm67mj5Lku7blip/og73pm4blkIhXZeW3neWkp+Wwj+eoi+W6jyBzY3VwbHVzIOS9v+eUqHdlcHnlvIDlj5HnmoTlrozlloTnmoTmoKHlm63nu7zlkIjlsI/nqIvluo8gNCDpobXpnaIg5YmN5ZCO56uv5byA5rqQIOWMheaLrOaIkOe7qSDor77ooagg5aSx54mp5oub6aKGIOWbvuS5pummhiDmlrDpl7votYTorq/nrYnnrYnluLjop4HmoKHlm63lnLrmma/lip/og73kuIDkuKrlhajpmo/mnLrnmoTliLfoo4XlpIflsI/muLjmiI/kuIDkuKp2dWXlhajlrrbmobblhaXpl6hEZW1vIOaYr+S4gOWAi+WPr+S7peW5q+WKqeaCqCBWdWUganMg55qE6aCF55uu5ris6Kmm5Y+K5YG16Yyv55qE5bel5YW3IOS5n+WQjOaZguaUr+aMgSBWdWV45Y+KIFZ1ZSBSb3V0ZXIg5b6u5L+h5YWs5LyX5Y+3566h55CG57O757ufIOWMheWQq+WFrOS8l+WPt+iPnOWNleeuoeeQhiDoh6rliqjlm57lpI0g57Sg5p2Q566h55CGIOaooeadv+a2iOaBryDnsonkuJ3nrqHnkIYg77iP562J5Yqf6IO9IOWJjeWQjuerr+mDveW8gOa6kOWFjei0uSDln7rkuo52dWUg55qE566h55CG5ZCO5Y+wIOmFjeWQiEJsb2cgQ29yZeS4jkJsb2cgVnVl562J5aSa5Liq6aG555uu5L2/55So5rW36aOO5bCP5bqXIOW8gOa6kOWVhuWfjiDlvq7kv6HlsI/nqIvluo/llYbln47nrqHnkIblkI7lj7Ag5ZCO5Y+w566h55CGIFZVRSBJVOS5i+WutuesrOS4ieaWueWwj+eoi+W6j+eJiOWuouaIt+erryDkvb/nlKggbXB2dWUg5byA5Y+RIOWFvOWuuSB3ZWIgdnVlIOWPr+S7peaLluaLveaOkuW6j+eahOagkeW9ouihqOagvCDnjrDku6MgV2ViIOW8gOWPkeivreazleWfuuehgOS4juW3peeoi+Wunui3tSDmtrXnm5YgV2ViIOW8gOWPkeWfuuehgCDliY3nq6/lt6XnqIvljJYg5bqU55So5p625p6EIOaAp+iDveS4juS9k+mqjOS8mOWMliDmt7flkIjlvIDlj5EgUmVhY3Qg5a6e6Le1IFZ1ZSDlrp7ot7UgV2ViQXNzZW1ibHkg562J5aSa5pa56Z2iIOaVsOaNruWkp+Wxj+WPr+inhuWMlue8lui+keWZqOS4gOS4qumAgueUqOS6juaRhOW9seS7juS4muiAhSDniLHlpb3ogIUg6K6+6K6h5biI562J5Yib5oSP6KGM5Lia5LuO5Lia6ICF55qE5Zu+5YOP5bel5YW3566xIOatpuaxieWkp+WtpuWbvuS5pummhuWKqeaJiyDmoYzpnaLnq6/ln7rkuo5mb3JtIGdlbmVyYXRvciDku7/pkonpkonlrqHmibnmtYHnqIvliJvlu7og6KGo5Y2V5Yib5bu6IOa1geeoi+iKgueCueWPr+inhuWMlumFjee9riDlv4XloavmnaHku7blj4rmoKHpqowg5LiA5Liq5a6M5pW0ZWxlY3Ryb27moYzpnaLorrDotKbnqIvluo8g5oqA5pyv5qCI5Li76KaB5L2/55SoZWxlY3Ryb24gdnVlIHZ1ZXRpZnkg5byA5py66Ieq5Yqo5ZCv5YqoIOiHquWKqOabtOaWsCDmiZjnm5jmnIDlsI/ljJYg6Zeq54OB562J5bi455So5Yqf6IO9IE5zaXPliLbkvZzmvILkuq7nmoTlronoo4XljIUg56iL5bqP54y/55qE5ama56S86YKA6K+35Ye9IOS4gOS4quWfuuS6jnZ1ZeWSjGVsZW1lbnQgdWnnmoTmoJHlvaLnqb/moq3moYblj4rpgq7ku7bpgJrorq/lvZXniYjmnKzop4HnpLrkvovop4Eg5Z+65LqOR2luIFZ1ZSBFbGVtZW50IFVJ55qE5YmN5ZCO56uv5YiG56a75p2D6ZmQ566h55CG57O757uf55qE5YmN56uv5qih5Z2X6YCa55So5Lmm57GN6ZiF6K+7QVBQIEJvb2tDaGF0IOeahCB1bmkgYXBwIOWunueOsOeJiOacrCDmlK/mjIHlpJrnq6/liIblj5Eg57yW6K+R55Sf5oiQQW5kcm9pZOWSjGlPUyDmiYvmnLpBUFDku6Xlj4rlkITlubPlj7DnmoTlsI/nqIvluo/ln7rkuo5WdWUz55qETWF0ZXJpYWwgZGVzaWdu6aOO5qC856e75Yqo56uv57uE5Lu25bqT6L+b6Zi26LWE5rex5YmN56uv5byA5Y+R5Zyo57q/6ICD6K+V57O757ufIHNwcmluZ2Jvb3QgdnVl5YmN5ZCO56uv5YiG56a755qE5LiA5Liq6aG555uuIO+4jyDml6DlkI7nq6/nmoTku78gWW91VHViZSBMaXZlIENoYXQg6aOO5qC855qE566A5piTIEJpbGliaWxpIOW8ueW5leWnrHZ1ZeWQjuerr+euoeeQhuezu+e7n+eVjOmdoiDln7rkuo51aee7hOS7tml2aWV3QmlsaWJpbGnnm7Tmkq3lvLnluZXlupMgZm9yIE1hYyBXaW5kb3dzIExpbnV4VnVl6auY5Lu/572R5piT5LqR6Z+z5LmQIOWfuuacrOWunueOsOe9keaYk+S6keaJgOaciemfs+S5kCBNVuebuOWFs+WKn+iDvSDnjrDlt7Lmm7TmlrDliLDnrKzkuozniYgg5LuF55So5LqO5a2m5LmgIOS4i+mdouacieivpue7huaVmeeoiyDmrabmsYnlpKflrablm77kuabppobliqnmiYsg56e75Yqo56uvWmV1c+WfuuS6jkdvbGFuZyBHaW4gY2FzYmluIOiHtOWKm+S6juWBmuS8geS4mue7n+S4gOadg+mZkCDotKblj7fkuK3lv4PnrqHnkIbns7vnu58g5YyF5ZCr6LSm5Y+3566h55CGIOaVsOaNruadg+mZkCDlip/og73mnYPpmZAg5bqU55So566h55CGIOWkmuaVsOaNruW6k+mAgumFjSDlj69kb2NrZXIg5LiA6ZSu6L+Q6KGMIOekvuWMuua0u+i3gyDniYjmnKzov63ku6Plv6sg5Yqg576k5YWN6LS55oqA5pyv5pSv5oyBIFZ1ZemrmOS7v+e9keaYk+S6kemfs+S5kCBWdWXlhaXpl6jlrp7ot7Ug5Zyo57q/6aKE6KeIIOaaguaXtuWBnOatouWfuuS6jiBWdWUg5ZKMIEVsZW1lbnRVSSDmnoTlu7rnmoTkuIDkuKrkvIHkuJrnuqflkI7lj7DnrqHnkIbns7vnu58g77iPIOi3qOW5s+WPsOenu+WKqOerr+inhumikei1hOa6kOaSreaUvuWZqCDnroDmtIHlhY3otLkgWlkgUGxheWVyIOenu+WKqOerryBBUFAg5Z+65LqOIFVuaSBhcHAg5byA5Y+RIFZ1ZeWunuaImOmhueebruWfuuS6juWPguiAg+Wwj+exs+WVhuWfjiDlrp7njrDnmoTnlLXllYbpobnnm64gaDXliLbkvZwg56e75Yqo56uv5LiT6aKY5rS75Yqo6aG16Z2i5Y+v6KeG5YyW57yW6L6R5Lu/6ZKJ6ZKJ5a6h5om55rWB56iL6K6+572u5Yqo5oCB6KGo5Y2V6aG16Z2i6K6+6K6hIOiHquWKqOeUn+aIkOmhtemdouW+ruWJjeerr+mhueebruWunuaImHZ1ZemhueebriDln7rkuo52dWUzIHFpYW5rdW4yIOi/m+mYtueJiCBnaXRodWIgY29tIHdsIHVpIHdsIG1mZeWfuuS6jiBkMiBhZG1pbueahFJCQUPmnYPpmZDnrqHnkIbop6PlhrPmlrnmoYhWdWVOb2RlIOaYr+S4gOWll+WfuuS6jueahOWJjeWQjuerr+WIhuemu+mhueebriDln7rkuo7ku7/kuqzkuJzmt5jlrp3nmoQg56e75Yqo56uvSDXnlLXllYblubPlj7Ag5beo5qCRIOWfuuS6jnp0cmVl5bCB6KOF55qEVnVl5qCR5b2i57uE5Lu2IOi9u+advuWunueOsOa1t+mHj+aVsOaNrueahOmrmOaAp+iDvea4suafkyDlvq7kv6HnuqLljIXlsIHpnaLpooblj5Yg55So5oi36KeC55yL6KeG6aKR5bm/5ZGK5oiW6ICF6YKA6K+355So5oi35Y+v6I635Y+W5b6u5L+h57qi5YyF5bqP5YiX5Y+35Z+65LqOIFZ1ZSDnmoTlj6/op4bljJbluIPlsYDnvJbovpHlmajmj5Lku7ZrYm9uZSB1aSDmmK/kuIDlpZfog73lkIzml7bmlK/mjIEg5bCP56iL5bqPIGtib25lIOWSjCB2dWUg5qGG5p625byA5Y+R55qE5aSa56uvIFVJIOW6kyBQUyDmlrDniYgga2JvbmUgdWkg5bey5Ye654KJ5bm26L+B56e75YiwIGtib25lIOS4u+S7k+W6kyDmraTku5PlupPku4XlgZrml6fniYjnu7TmiqTkuYvnlKgg5LiA5LiqdnVl55qE5Liq5Lq65Y2a5a6i6aG555uuIOmFjeWQiCBuZXQgY29yZSBhcGnmlZnnqIsg5omT6YCg5YmN5ZCO56uv5YiG56a7VHVtbyBCbG9nIEZvciBWdWUganMg5YmN5ZCO56uv5YiG56a7YnBtbiBqc+a1geeoi+iuvuiuoeWZqOe7hOS7tiDln7rkuo52dWUgZWxlbWVudHVp576O5YyW5bGe5oCn6Z2i5p2/IOa7oei2szkgJeS7peS4iueahOS4muWKoemcgOaxguS4k+mXqOS4uiBXZWV4IOWJjeerr+W8gOWPkeiAheaJk+mAoOeahOS4gOWll+mrmOi0qOmHj1VJ5qGG5p62IOaDs+eUqHZ1ZeaKiuaIkeeOsOWcqOeahOS4quS6uue9keermemHjeaWsOWGmeS4gOS4iyDmlrDnmoTpo47moLwg5paw55qE5oqA5pyvIOS7gOS5iOmDveaYr+aWsOeahCDmnKzpobnnm67mmK/kuIDkuKrlnKjnur/ogYrlpKnns7vnu58g5pyA5aSn56iL5bqm55qE6L+Y5Y6f5LqGTWFj5a6i5oi356uvUVEgdnVlIGNsaTMg5ZCO5Y+w566h55CG5qih5p2/IGhlYXJ0IOWfuuS6jnZ1ZTLlkox2dWV455qE5aSN5p2C5Y2V6aG16Z2i5bqU55SoIDIg6aG16Z2iNTPkuKpBUEkg5Lu/5a6e6aqM5qW8IOWfuuS6jiBWdWUgS29hIOeahCBXZWJEZXNrdG9wIOinhueql+ezu+e7nyBKZWViYXNl5piv5LiA5qy+5YmN5ZCO56uv5YiG56a755qE5byA5rqQ5byA5Y+R5qGG5p62IOWfuuS6juW8gOWPkSDkuIDlpZdTcHJpbmdCb2905ZCO5Y+wIOS4pOWll+WJjeerr+mhtemdoiDlj6/ku6Xoh6rnlLHpgInmi6nln7rkuo5FbGVtZW50VUnmiJbogIVBbnREZXNpZ27nmoTliY3nq6/nlYzpnaIg5LqM5pyf5Lya5pW05ZCIcmVhY3TliY3nq6/moYbmnrYgQW50IERlc2lnbiBSZWFjdCDlnKjlrp7pmYXlupTnlKjkuK3lt7Lnu4/kvb/nlKjov5nlpZfmoYbmnrblvIDlj5HkuoZDTVPnvZHnq5nns7vnu58g56S+5Yy66K665Z2b57O757ufIOW+ruS/oeWwj+eoi+W6jyDlvq7kv6HmnI3liqHlj7fnrYkg5ZCO6Z2i5Lya6YCQ5q2l5pW055CG5byA5rqQIOacrOmhueebruS4u+imgeebrueahOWcqOS6juaVtOWQiOS4u+a1geaKgOacr+ahhuaetiDlr7vmib7lupTnlKjmnIDkvbPpobnnm67lrp7ot7XmlrnmoYgg5a6e546w5Y+v55u05o6l5L2/55So55qE5b+r6YCf5byA5Y+R5qGG5p62IOS9v+eUqCB2dWUgY2xpMyDmkK3lu7rnmoR2dWUgdnVleCByb3V0ZXIgZWxlbWVudCDlvIDlj5HmqKHniYgg6ZuG5oiQ5bi455So57uE5Lu2IOWKn+iDveaooeWdl0pFRUNHIEJPT1QgQVBQIOenu+WKqOino+WGs+aWueahiCDph4fnlKh1bmlhcHDmoYbmnrYg5LiA5Lu95Luj56CB5aSa57uI56uv6YCC6YWNIOWQjOaXtuaUr+aMgUFQUCDlsI/nqIvluo8gSDUg5a6e546w5LqG5LiOSmVlY2dCb2905bmz5Y+w5a6M576O5a+55o6l55qE56e75Yqo6Kej5Yaz5pa55qGIIOebruWJjeW3sue7j+WunueOsOeZu+W9lSDnlKjmiLfkv6Hmga8g6YCa6K6v5b2VIOWFrOWRiiDnp7vliqjpppbpobUg5Lmd5a6r5qC8562J5Z+656GA5Yqf6IO9IOaYjuaXpeaWueiIn+W3peWFt+eusSDmlK/mjIHkuK3lj7Dnvo7ml6Xpn6nmnI12dWXnmoTpqozor4HnoIHmj5Lku7bov5nph4zmnInkuIDkupvmoIflh4bnu4Tku7blupPlj6/og73msqHmnInnmoTlip/og73nu4Tku7Yg5bey5pyJ57uE5Lu2IOaUvuWkp+mVnCDnrb7liLAg5Zu+54mH5qCH562+IOa7keWKqOmqjOivgSDlgJLorqHml7Yg5rC05Y2wIOaLluaLvSDlpKflrrbmnaXmib7ojKwg5Z+65LqOVnVlMiBOb2RlanMgTXlTUUznmoTljZrlrqIg5pyJ5ZCO5Y+w566h55CG57O757ufXCIsXCJodG1sX3VybFwiOlwiaHR0cHM6Ly9naXRodWIuY29tL2dlZ2UtY2lyY2xlLy5naXRodWJcIixcInN0YXJnYXplcnNfY291bnRcIjoxNjgwLFwiZm9ya3NfY291bnRcIjoxNDQsXCJvcGVuX2lzc3Vlc19jb3VudFwiOjkxOSxcInVwZGF0ZWRfYXRcIjpcIjIwMjUtMTEtMTlUMTI6NTA6MzNaXCIsXCJjcmVhdGVkX2F0XCI6XCIyMDIwLTA5LTIwVDA0OjQ3OjI1WlwiLFwidG9waWNzXCI6W1wiYS1zb3VsXCIsXCJhY2Z1blwiLFwiYmlsaWJpbGlcIixcImNoaW5hXCIsXCJnZWdlLWNpcmNsZVwiLFwibWVzc2FnZS1ib2FyZFwiLFwidnR1YmVyXCIsXCJ2dXBcIl0sXCJwcml2YXRlXCI6ZmFsc2UsXCJmb3JrXCI6ZmFsc2UsXCJhcmNoaXZlZFwiOmZhbHNlLFwiZGVmYXVsdF9icmFuY2hcIjpcIm1hc3RlclwifSx7XCJpZFwiOjE2MjkxOTg5MCxcIm5hbWVcIjpcImF3ZXNvbWUtYWktbWwtZGxcIixcImZ1bGxfbmFtZVwiOlwibmVvbWF0cml4MzY5L2F3ZXNvbWUtYWktbWwtZGxcIixcImRlc2NyaXB0aW9uXCI6XCJBd2Vzb21lIEFydGlmaWNpYWwgSW50ZWxsaWdlbmNlLCBNYWNoaW5lIExlYXJuaW5nIGFuZCBEZWVwIExlYXJuaW5nIGFzIHdlIGxlYXJuIGl0LiBTdHVkeSBub3RlcyBhbmQgYSBjdXJhdGVkIGxpc3Qgb2YgYXdlc29tZSByZXNvdXJjZXMgb2Ygc3VjaCB0b3BpY3MuXCIsXCJodG1sX3VybFwiOlwiaHR0cHM6Ly9naXRodWIuY29tL25lb21hdHJpeDM2OS9hd2Vzb21lLWFpLW1sLWRsXCIsXCJsYW5ndWFnZVwiOlwiSnVweXRlciBOb3RlYm9va1wiLFwic3RhcmdhemVyc19jb3VudFwiOjE2MDgsXCJmb3Jrc19jb3VudFwiOjM2MSxcIm9wZW5faXNzdWVzX2NvdW50XCI6MyxcInVwZGF0ZWRfYXRcIjpcIjIwMjUtMTEtMTlUMTc6MTc6MjZaXCIsXCJjcmVhdGVkX2F0XCI6XCIyMDE4LTEyLTIzVDE5OjU1OjQ5WlwiLFwidG9waWNzXCI6W1wiYWlcIixcImFsZ29yaXRobXNcIixcImFydGlmaWNpYWwtaW50ZWxsaWdlbmNlXCIsXCJjbG91ZC1kZXZvcHNcIixcImRhdGFcIixcImRhdGEtZ2VuZXJhdGlvblwiLFwiZGVlcC1sZWFybmluZ1wiLFwiZGxcIixcImRvY2tlclwiLFwiZ3JhYWxcIixcImdyYWFsdm1cIixcImludGVsbGlnZW50LXN5c3RlbXNcIixcIm1hY2hpbmUtaW50ZWxsaWdlbmNlXCIsXCJtYWNoaW5lLWxlYXJuaW5nXCIsXCJtYXRoZW1hdGljYVwiLFwibWxcIixcIm5hdHVyYWwtbGFuZ3VhZ2UtcHJvY2Vzc2luZ1wiLFwibmV1cmFsLW5ldHdvcmtzXCIsXCJubHBcIixcInRpbWUtc2VyaWVzXCJdLFwicHJpdmF0ZVwiOmZhbHNlLFwiZm9ya1wiOmZhbHNlLFwiYXJjaGl2ZWRcIjpmYWxzZSxcImRlZmF1bHRfYnJhbmNoXCI6XCJtYXN0ZXJcIn0se1wiaWRcIjozNzY3NDA0NDksXCJuYW1lXCI6XCJ5dC1jaGFubmVscy1EUy1BSS1NTC1DU1wiLFwiZnVsbF9uYW1lXCI6XCJiZW50aGVjb2Rlci95dC1jaGFubmVscy1EUy1BSS1NTC1DU1wiLFwiZGVzY3JpcHRpb25cIjpcIkEgY29tcHJlaGVuc2l2ZSBsaXN0IG9mIDE4MCsgWW91VHViZSBDaGFubmVscyBmb3IgRGF0YSBTY2llbmNlLCAgRGF0YSBFbmdpbmVlcmluZywgTWFjaGluZSBMZWFybmluZywgRGVlcCBsZWFybmluZywgQ29tcHV0ZXIgU2NpZW5jZSwgcHJvZ3JhbW1pbmcsIHNvZnR3YXJlIGVuZ2luZWVyaW5nLCBldGMuXCIsXCJodG1sX3VybFwiOlwiaHR0cHM6Ly9naXRodWIuY29tL2JlbnRoZWNvZGVyL3l0LWNoYW5uZWxzLURTLUFJLU1MLUNTXCIsXCJzdGFyZ2F6ZXJzX2NvdW50XCI6MTM3NyxcImZvcmtzX2NvdW50XCI6MTQxLFwib3Blbl9pc3N1ZXNfY291bnRcIjoxLFwidXBkYXRlZF9hdFwiOlwiMjAyNS0xMS0xNVQwMToyNTo0OVpcIixcImNyZWF0ZWRfYXRcIjpcIjIwMjEtMDYtMTRUMDc6MzE6MjZaXCIsXCJ0b3BpY3NcIjpbXCJhaVwiLFwiYXJ0aWZpY2lhbC1pbnRlbGxpZ2VuY2VcIixcImF3ZXNvbWVcIixcImF3ZXNvbWUtbGlzdFwiLFwiY29kaW5nXCIsXCJkYXRhXCIsXCJkYXRhLWFuYWx5c2lzXCIsXCJkYXRhLWVuZ2luZWVyaW5nXCIsXCJkYXRhLXNjaWVuY2VcIixcImRlZXAtbGVhcm5pbmdcIixcIm1hY2hpbmUtbGVhcm5pbmdcIixcIm1hdGhcIixcIm1sXCIsXCJwcm9ncmFtbWluZ1wiLFwicHl0aG9uXCIsXCJyZXNvdXJjZXNcIixcInNvZnR3YXJlLWVuZ2luZWVyaW5nXCIsXCJzdGF0aXN0aWNzXCIsXCJ3ZWItZGV2ZWxvcG1lbnRcIixcInlvdXR1YmVcIl0sXCJwcml2YXRlXCI6ZmFsc2UsXCJmb3JrXCI6ZmFsc2UsXCJhcmNoaXZlZFwiOmZhbHNlLFwiZGVmYXVsdF9icmFuY2hcIjpcIm1hc3RlclwifV19In1dfSx7InR5cGUiOiJtY3BfdG9vbF9yZXN1bHQiLCJ0b29sX3VzZV9pZCI6Im1jcHRvb2x1XzAxQVA2TUU4ejFBd0dDdXpXNFJrcnFOTCIsImlzX2Vycm9yIjpmYWxzZSwiY29udGVudCI6W3sidHlwZSI6InRleHQiLCJ0ZXh0Ijoie1widG90YWxfY291bnRcIjozNjcsXCJpbmNvbXBsZXRlX3Jlc3VsdHNcIjpmYWxzZSxcIml0ZW1zXCI6W3tcImlkXCI6NzQwMzAzNjg2LFwibmFtZVwiOlwic2dsYW5nXCIsXCJmdWxsX25hbWVcIjpcInNnbC1wcm9qZWN0L3NnbGFuZ1wiLFwiZGVzY3JpcHRpb25cIjpcIlNHTGFuZyBpcyBhIGZhc3Qgc2VydmluZyBmcmFtZXdvcmsgZm9yIGxhcmdlIGxhbmd1YWdlIG1vZGVscyBhbmQgdmlzaW9uIGxhbmd1YWdlIG1vZGVscy5cIixcImh0bWxfdXJsXCI6XCJodHRwczovL2dpdGh1Yi5jb20vc2dsLXByb2plY3Qvc2dsYW5nXCIsXCJsYW5ndWFnZVwiOlwiUHl0aG9uXCIsXCJzdGFyZ2F6ZXJzX2NvdW50XCI6MjAyMjYsXCJmb3Jrc19jb3VudFwiOjM0NTMsXCJvcGVuX2lzc3Vlc19jb3VudFwiOjE0NTMsXCJ1cGRhdGVkX2F0XCI6XCIyMDI1LTExLTE5VDIxOjIwOjM1WlwiLFwiY3JlYXRlZF9hdFwiOlwiMjAyNC0wMS0wOFQwNDoxNTo1MlpcIixcInRvcGljc1wiOltcImJsYWNrd2VsbFwiLFwiY3VkYVwiLFwiZGVlcHNlZWtcIixcImRlZXBzZWVrLXIxXCIsXCJkZWVwc2Vlay12M1wiLFwiZGVlcHNlZWstdjMtMlwiLFwiZ3B0LW9zc1wiLFwiaW5mZXJlbmNlXCIsXCJraW1pXCIsXCJsbGFtYVwiLFwibGxhbWEzXCIsXCJsbGF2YVwiLFwibGxtXCIsXCJsbG0tc2VydmluZ1wiLFwibW9lXCIsXCJvcGVuYWlcIixcInB5dG9yY2hcIixcInF3ZW4zXCIsXCJ0cmFuc2Zvcm1lclwiLFwidmxtXCJdLFwicHJpdmF0ZVwiOmZhbHNlLFwiZm9ya1wiOmZhbHNlLFwiYXJjaGl2ZWRcIjpmYWxzZSxcImRlZmF1bHRfYnJhbmNoXCI6XCJtYWluXCJ9LHtcImlkXCI6MTAwMTkzNTY4NSxcIm5hbWVcIjpcIkxMTXMtZnJvbS1zY3JhdGNoXCIsXCJmdWxsX25hbWVcIjpcIkxhbW9yYXRpOTIvTExNcy1mcm9tLXNjcmF0Y2hcIixcImRlc2NyaXB0aW9uXCI6XCLwn5OaIEJ1aWxkIGFuZCB0cmFpbiB5b3VyIG93biBHUFQtbGlrZSBMYXJnZSBMYW5ndWFnZSBNb2RlbCBmcm9tIHNjcmF0Y2ggd2l0aCBjbGVhciBndWlkYW5jZSBhbmQgcmVhbCBjb2RlIGV4YW1wbGVzLlwiLFwiaHRtbF91cmxcIjpcImh0dHBzOi8vZ2l0aHViLmNvbS9MYW1vcmF0aTkyL0xMTXMtZnJvbS1zY3JhdGNoXCIsXCJsYW5ndWFnZVwiOlwiSnVweXRlciBOb3RlYm9va1wiLFwic3RhcmdhemVyc19jb3VudFwiOjEsXCJmb3Jrc19jb3VudFwiOjAsXCJvcGVuX2lzc3Vlc19jb3VudFwiOjAsXCJ1cGRhdGVkX2F0XCI6XCIyMDI1LTExLTE5VDE5OjQ2OjM3WlwiLFwiY3JlYXRlZF9hdFwiOlwiMjAyNS0wNi0xNFQxMTowMzoyM1pcIixcInRvcGljc1wiOltcImFkdmFuY2VkLW5ldXJhbC1uZXR3b3JrXCIsXCJhZ2VudHNcIixcImJvb2tcIixcImNsYXNzaWZpY2F0aW9uXCIsXCJkZWVwLWxlYXJuaW5nXCIsXCJkZWVwLW5ldXJhbC1uZXR3b3Jrc1wiLFwiZmxhbi10NVwiLFwiZnJvbS1zY3JhdGNoXCIsXCJncHRcIixcImxhbmd1YWdlLW1vZGVsXCIsXCJsbG1cIixcImxsbXMtYm9va1wiLFwibWNwXCIsXCJubHBcIixcInByb21wdC1lbmdpbmVlcmluZ1wiLFwicm9iZXJ0YVwiLFwicnVzdFwiLFwidHJhbnNmb3JtZXJcIl0sXCJwcml2YXRlXCI6ZmFsc2UsXCJmb3JrXCI6ZmFsc2UsXCJhcmNoaXZlZFwiOmZhbHNlLFwiZGVmYXVsdF9icmFuY2hcIjpcIm1haW5cIn0se1wiaWRcIjo3MzU0MjUyMDMsXCJuYW1lXCI6XCJ0cmFuc2Zvcm1lcmxhYi1hcHBcIixcImZ1bGxfbmFtZVwiOlwidHJhbnNmb3JtZXJsYWIvdHJhbnNmb3JtZXJsYWItYXBwXCIsXCJkZXNjcmlwdGlvblwiOlwiT3BlbiBTb3VyY2UgQXBwbGljYXRpb24gZm9yIEFkdmFuY2VkIExMTSArIERpZmZ1c2lvbiBFbmdpbmVlcmluZzogaW50ZXJhY3QsIHRyYWluLCBmaW5lLXR1bmUsIGFuZCBldmFsdWF0ZSBsYXJnZSBsYW5ndWFnZSBtb2RlbHMgb24geW91ciBvd24gY29tcHV0ZXIuXCIsXCJodG1sX3VybFwiOlwiaHR0cHM6Ly9naXRodWIuY29tL3RyYW5zZm9ybWVybGFiL3RyYW5zZm9ybWVybGFiLWFwcFwiLFwibGFuZ3VhZ2VcIjpcIlB5dGhvblwiLFwic3RhcmdhemVyc19jb3VudFwiOjQ1MzMsXCJmb3Jrc19jb3VudFwiOjQ1NSxcIm9wZW5faXNzdWVzX2NvdW50XCI6NjIsXCJ1cGRhdGVkX2F0XCI6XCIyMDI1LTExLTE5VDE5OjQ1OjUyWlwiLFwiY3JlYXRlZF9hdFwiOlwiMjAyMy0xMi0yNFQyMjowOToxNFpcIixcInRvcGljc1wiOltcImRpZmZ1c2lvblwiLFwiZGlmZnVzaW9uLW1vZGVsc1wiLFwiZWxlY3Ryb25cIixcImxsYW1hXCIsXCJsbG1zXCIsXCJsb3JhXCIsXCJtbHhcIixcInJsaGZcIixcInN0YWJpbGl0eS1kaWZmdXNpb25cIixcInRyYW5zZm9ybWVyc1wiXSxcInByaXZhdGVcIjpmYWxzZSxcImZvcmtcIjpmYWxzZSxcImFyY2hpdmVkXCI6ZmFsc2UsXCJkZWZhdWx0X2JyYW5jaFwiOlwibWFpblwifSx7XCJpZFwiOjk3NjcwODc0OCxcIm5hbWVcIjpcInRyYW5zZm9ybWVybGFiLWFwcFwiLFwiZnVsbF9uYW1lXCI6XCJTT01JUjQyMC90cmFuc2Zvcm1lcmxhYi1hcHBcIixcImRlc2NyaXB0aW9uXCI6XCJPcGVuIFNvdXJjZSBBcHBsaWNhdGlvbiBmb3IgQWR2YW5jZWQgTExNIEVuZ2luZWVyaW5nOiBpbnRlcmFjdCwgdHJhaW4sIGZpbmUtdHVuZSwgYW5kIGV2YWx1YXRlIGxhcmdlIGxhbmd1YWdlIG1vZGVscyBvbiB5b3VyIG93biBjb21wdXRlci5cIixcImh0bWxfdXJsXCI6XCJodHRwczovL2dpdGh1Yi5jb20vU09NSVI0MjAvdHJhbnNmb3JtZXJsYWItYXBwXCIsXCJsYW5ndWFnZVwiOlwiVHlwZVNjcmlwdFwiLFwic3RhcmdhemVyc19jb3VudFwiOjEsXCJmb3Jrc19jb3VudFwiOjIsXCJvcGVuX2lzc3Vlc19jb3VudFwiOjEsXCJ1cGRhdGVkX2F0XCI6XCIyMDI1LTExLTE5VDE4OjQxOjQ0WlwiLFwiY3JlYXRlZF9hdFwiOlwiMjAyNS0wNS0wMlQxNTo0ODowOFpcIixcInRvcGljc1wiOltcImVsZWN0cm9uXCIsXCJsbGFtYVwiLFwibG9yYVwiLFwibWx4XCIsXCJybGhmXCIsXCJ0cmFuc2Zvcm1lcnNcIl0sXCJwcml2YXRlXCI6ZmFsc2UsXCJmb3JrXCI6ZmFsc2UsXCJhcmNoaXZlZFwiOmZhbHNlLFwiZGVmYXVsdF9icmFuY2hcIjpcIm1haW5cIn0se1wiaWRcIjoxMDg4MTA2OTAzLFwibmFtZVwiOlwiTExNLUV2b2x1dGlvbi1NZXRhZGF0YS1SZXBvc2l0b3J5XCIsXCJmdWxsX25hbWVcIjpcIkhhcnNpZGFrL0xMTS1Fdm9sdXRpb24tTWV0YWRhdGEtUmVwb3NpdG9yeVwiLFwiZGVzY3JpcHRpb25cIjpcIlRoaXMgcmVwb3NpdHJ5IFNob3djYXNlcyB0aGUgaGlzdG9yaWNhbCB0aW1lbGluZSBvZiBMYXJnZSBMYW5ndWFnZSBNb2RlbHMoTExNcykgZnJvbSB0aGUgdHJhbnNmb3JtZXJzIG1vZGVsIHRvIGN1cnJlbnQgc3RhdGUgb2YgYXJ0IG1vZGVsc1wiLFwiaHRtbF91cmxcIjpcImh0dHBzOi8vZ2l0aHViLmNvbS9IYXJzaWRhay9MTE0tRXZvbHV0aW9uLU1ldGFkYXRhLVJlcG9zaXRvcnlcIixcImxhbmd1YWdlXCI6XCJKdXB5dGVyIE5vdGVib29rXCIsXCJzdGFyZ2F6ZXJzX2NvdW50XCI6MCxcImZvcmtzX2NvdW50XCI6MCxcIm9wZW5faXNzdWVzX2NvdW50XCI6MCxcInVwZGF0ZWRfYXRcIjpcIjIwMjUtMTEtMTdUMDY6NDQ6NTVaXCIsXCJjcmVhdGVkX2F0XCI6XCIyMDI1LTExLTAyVDEwOjI2OjI4WlwiLFwicHJpdmF0ZVwiOmZhbHNlLFwiZm9ya1wiOmZhbHNlLFwiYXJjaGl2ZWRcIjpmYWxzZSxcImRlZmF1bHRfYnJhbmNoXCI6XCJtYWluXCJ9LHtcImlkXCI6MTA5NjgzOTcwOCxcIm5hbWVcIjpcImdwdS1sbG0tb24td3NsXCIsXCJmdWxsX25hbWVcIjpcImNvbGlubXhzL2dwdS1sbG0tb24td3NsXCIsXCJkZXNjcmlwdGlvblwiOlwiRG9ja2VyaXplZCBlbnZpcm9ubWVudCBmb3IgcnVubmluZyBxdWFudGl6ZWQgbGFyZ2UgbGFuZ3VhZ2UgbW9kZWxzIHdpdGggR1BVIGFjY2VsZXJhdGlvbiBvbiBXaW5kb3dzIHZpYSBXU0wyLiBJbmNsdWRlcyBQeVRvcmNoLCBIdWdnaW5nIEZhY2UgVHJhbnNmb3JtZXJzLCBhbmQgYml0c2FuZGJ5dGVzIGZvciBlZmZpY2llbnQgbG9jYWwgaW5mZXJlbmNlIHdpdGggTGxhbWHigK8yLCBNaXN0cmFsLCBhbmQgb3RoZXIgN0IvMTNCIG1vZGVscy5cIixcImh0bWxfdXJsXCI6XCJodHRwczovL2dpdGh1Yi5jb20vY29saW5teHMvZ3B1LWxsbS1vbi13c2xcIixcImxhbmd1YWdlXCI6XCJQeXRob25cIixcInN0YXJnYXplcnNfY291bnRcIjowLFwiZm9ya3NfY291bnRcIjowLFwib3Blbl9pc3N1ZXNfY291bnRcIjowLFwidXBkYXRlZF9hdFwiOlwiMjAyNS0xMS0xN1QwMzoyNjoxOFpcIixcImNyZWF0ZWRfYXRcIjpcIjIwMjUtMTEtMTVUMDM6MjQ6NDhaXCIsXCJwcml2YXRlXCI6ZmFsc2UsXCJmb3JrXCI6ZmFsc2UsXCJhcmNoaXZlZFwiOmZhbHNlLFwiZGVmYXVsdF9icmFuY2hcIjpcIm1haW5cIn0se1wiaWRcIjoxMDk3Nzk2OTIzLFwibmFtZVwiOlwibmxwLWxsbS1qb3VybmV5XCIsXCJmdWxsX25hbWVcIjpcIlZpc2hha2hhOTMvbmxwLWxsbS1qb3VybmV5XCIsXCJkZXNjcmlwdGlvblwiOlwiTGVhcm5pbmcgbG9nIGFuZCBjb2RlIHBsYXlncm91bmQgZm9yIG15IGpvdXJuZXkgZnJvbSBjb3JlIE5MUCBjb25jZXB0cyAodG9rZW5pemF0aW9uLCBCUEUsIGxhbmd1YWdlIG1vZGVsaW5nKSB0byB0cmFuc2Zvcm1lciBpbnRlcm5hbHMgYW5kIGxhcmdlIGxhbmd1YWdlIG1vZGVsIHN5c3RlbXMgKEdQVSBtZW1vcnksIG1vZGVsIHBhcmFsbGVsaXNtLCBMb1JBLCBkZXBsb3ltZW50KS5cIixcImh0bWxfdXJsXCI6XCJodHRwczovL2dpdGh1Yi5jb20vVmlzaGFraGE5My9ubHAtbGxtLWpvdXJuZXlcIixcInN0YXJnYXplcnNfY291bnRcIjowLFwiZm9ya3NfY291bnRcIjowLFwib3Blbl9pc3N1ZXNfY291bnRcIjowLFwidXBkYXRlZF9hdFwiOlwiMjAyNS0xMS0xNlQyMTozMDo1OVpcIixcImNyZWF0ZWRfYXRcIjpcIjIwMjUtMTEtMTZUMjA6NTA6MDJaXCIsXCJwcml2YXRlXCI6ZmFsc2UsXCJmb3JrXCI6ZmFsc2UsXCJhcmNoaXZlZFwiOmZhbHNlLFwiZGVmYXVsdF9icmFuY2hcIjpcIm1haW5cIn0se1wiaWRcIjoxMDk2OTUxOTgyLFwibmFtZVwiOlwiR2VuZXJhdGl2ZS1BSS1Bc3NpZ25tZW50LVVzaW5nLWdvb2dsZV9mbGFuX3Q1X3NtYWxsXCIsXCJmdWxsX25hbWVcIjpcIkJhcnJjYXJkYXZpcy9HZW5lcmF0aXZlLUFJLUFzc2lnbm1lbnQtVXNpbmctZ29vZ2xlX2ZsYW5fdDVfc21hbGxcIixcImRlc2NyaXB0aW9uXCI6XCJUaGUgZ29hbCBvZiB0aGlzIGFzc2lnbm1lbnQgaXMgdG8gZ2FpbiBwcmFjdGljYWwgZXhwZXJpZW5jZSB1c2luZyBzdGF0ZS1vZi10aGUtYXJ0IExhcmdlIExhbmd1YWdlIE1vZGVscyAoTExNcykgdmlhIHRoZSBIdWdnaW5nIEZhY2UgVHJhbnNmb3JtZXJzIGxpYnJhcnkuIFlvdSB3aWxsIHNlbGVjdCBhIG1vZGVsIGFuZCBpbXBsZW1lbnQgb25lIGNvcmUgZ2VuZXJhdGl2ZSB0YXNrLCBleHBlcmltZW50aW5nIHdpdGgga2V5IHBhcmFtZXRlcnMuXCIsXCJodG1sX3VybFwiOlwiaHR0cHM6Ly9naXRodWIuY29tL0JhcnJjYXJkYXZpcy9HZW5lcmF0aXZlLUFJLUFzc2lnbm1lbnQtVXNpbmctZ29vZ2xlX2ZsYW5fdDVfc21hbGxcIixcImxhbmd1YWdlXCI6XCJKdXB5dGVyIE5vdGVib29rXCIsXCJzdGFyZ2F6ZXJzX2NvdW50XCI6MCxcImZvcmtzX2NvdW50XCI6MCxcIm9wZW5faXNzdWVzX2NvdW50XCI6MCxcInVwZGF0ZWRfYXRcIjpcIjIwMjUtMTEtMTZUMjA6Mjg6NTdaXCIsXCJjcmVhdGVkX2F0XCI6XCIyMDI1LTExLTE1VDA4OjUwOjIzWlwiLFwicHJpdmF0ZVwiOmZhbHNlLFwiZm9ya1wiOmZhbHNlLFwiYXJjaGl2ZWRcIjpmYWxzZSxcImRlZmF1bHRfYnJhbmNoXCI6XCJtYWluXCJ9LHtcImlkXCI6MTAzMzY4NjQ0NSxcIm5hbWVcIjpcIkdFTi1BSS1DSEFUQk9UXCIsXCJmdWxsX25hbWVcIjpcIlNINElLVlQvR0VOLUFJLUNIQVRCT1RcIixcImRlc2NyaXB0aW9uXCI6XCJBIGNvbnZlcnNhdGlvbmFsIGNoYXRib3QgYnVpbHQgd2l0aCBMYW5nQ2hhaW4sIEh1Z2dpbmcgRmFjZSBUcmFuc2Zvcm1lcnMsIGFuZCBTdHJlYW1saXQsIGRlc2lnbmVkIHRvIGludGVyYWN0IHdpdGggdXNlcnMgaW50ZWxsaWdlbnRseSB1c2luZyBwb3dlcmZ1bCBMTE1zIGxpa2UgZmxhbi10NS4gVGhpcyBwcm9qZWN0IGRlbW9uc3RyYXRlcyBob3cgdG8gaW50ZWdyYXRlIGxhcmdlIGxhbmd1YWdlIG1vZGVscyBpbnRvIGEgd2ViIGludGVyZmFjZSBmb3IgbmF0dXJhbCBsYW5ndWFnZSB1bmRlcnN0YW5kaW5nLCBxdWVzdGlvbiBhbnN3ZXJpbmcsIGFuZCBlZHVjYXRpb25hbCBzdXBwb3J0LlwiLFwiaHRtbF91cmxcIjpcImh0dHBzOi8vZ2l0aHViLmNvbS9TSDRJS1ZUL0dFTi1BSS1DSEFUQk9UXCIsXCJsYW5ndWFnZVwiOlwiUHl0aG9uXCIsXCJzdGFyZ2F6ZXJzX2NvdW50XCI6MCxcImZvcmtzX2NvdW50XCI6MCxcIm9wZW5faXNzdWVzX2NvdW50XCI6MCxcInVwZGF0ZWRfYXRcIjpcIjIwMjUtMTEtMTVUMTE6NTY6MDRaXCIsXCJjcmVhdGVkX2F0XCI6XCIyMDI1LTA4LTA3VDA3OjM4OjEyWlwiLFwicHJpdmF0ZVwiOmZhbHNlLFwiZm9ya1wiOmZhbHNlLFwiYXJjaGl2ZWRcIjpmYWxzZSxcImRlZmF1bHRfYnJhbmNoXCI6XCJtYWluXCJ9LHtcImlkXCI6MTA4MzEwMzI5NSxcIm5hbWVcIjpcIlByb21wdC1JbmplY3Rpb24tRGV0ZWN0b3JcIixcImZ1bGxfbmFtZVwiOlwiS2lyYW5lc3dhci9Qcm9tcHQtSW5qZWN0aW9uLURldGVjdG9yXCIsXCJkZXNjcmlwdGlvblwiOlwiQSBtYWNoaW5lIGxlYXJuaW5nIG1vZGVsIHRvIGRldGVjdCBhbmQgcHJldmVudCBwcm9tcHQgaW5qZWN0aW9uIGF0dGFja3MgaW4gTGFyZ2UgTGFuZ3VhZ2UgTW9kZWxzIChMTE1zKS4gVXNlcyBEaXN0aWxCRVJUIGZvciBiaW5hcnkgY2xhc3NpZmljYXRpb24gb2Ygc2FmZSB2cy4gbWFsaWNpb3VzIHByb21wdHMsIGFjaGlldmluZyA5OSUgYWNjdXJhY3kgaW4gaWRlbnRpZnlpbmcgamFpbGJyZWFrIGF0dGVtcHRzIGFuZCBoYXJtZnVsIGluc3RydWN0aW9ucy4gQnVpbHQgd2l0aCBQeVRvcmNoIGFuZCBIdWdnaW5nIEZhY2UgVHJhbnNmb3JtZXJzLlwiLFwiaHRtbF91cmxcIjpcImh0dHBzOi8vZ2l0aHViLmNvbS9LaXJhbmVzd2FyL1Byb21wdC1JbmplY3Rpb24tRGV0ZWN0b3JcIixcImxhbmd1YWdlXCI6XCJQeXRob25cIixcInN0YXJnYXplcnNfY291bnRcIjowLFwiZm9ya3NfY291bnRcIjowLFwib3Blbl9pc3N1ZXNfY291bnRcIjowLFwidXBkYXRlZF9hdFwiOlwiMjAyNS0xMS0xNVQwNzowMToyNFpcIixcImNyZWF0ZWRfYXRcIjpcIjIwMjUtMTAtMjVUMTA6NTY6NTVaXCIsXCJwcml2YXRlXCI6ZmFsc2UsXCJmb3JrXCI6ZmFsc2UsXCJhcmNoaXZlZFwiOmZhbHNlLFwiZGVmYXVsdF9icmFuY2hcIjpcIm1haW5cIn0se1wiaWRcIjoxMDc4NTA2ODE4LFwibmFtZVwiOlwiTExNLWZyb20tU2NyYXRjaFwiLFwiZnVsbF9uYW1lXCI6XCJzYWVlZG1vaHNlbmk5Ny9MTE0tZnJvbS1TY3JhdGNoXCIsXCJkZXNjcmlwdGlvblwiOlwiRnJvbSB0b2tlbml6ZXIgdG8gdHJhbnNmb3JtZXIg4oCUIGEgaGFuZHMtb24gZXhwbG9yYXRpb24gb2YgaG93IGxhcmdlIGxhbmd1YWdlIG1vZGVscyBsZWFybiwgaW1wbGVtZW50ZWQgYW5kIHRyYWluZWQgZW50aXJlbHkgZnJvbSBzY3JhdGNoIG9uIFRpbnlTdG9yaWVzIHVzaW5nIFB5VG9yY2guIFwiLFwiaHRtbF91cmxcIjpcImh0dHBzOi8vZ2l0aHViLmNvbS9zYWVlZG1vaHNlbmk5Ny9MTE0tZnJvbS1TY3JhdGNoXCIsXCJsYW5ndWFnZVwiOlwiSnVweXRlciBOb3RlYm9va1wiLFwic3RhcmdhemVyc19jb3VudFwiOjAsXCJmb3Jrc19jb3VudFwiOjAsXCJvcGVuX2lzc3Vlc19jb3VudFwiOjAsXCJ1cGRhdGVkX2F0XCI6XCIyMDI1LTExLTE0VDIyOjI1OjQzWlwiLFwiY3JlYXRlZF9hdFwiOlwiMjAyNS0xMC0xN1QyMTowNToyNlpcIixcInByaXZhdGVcIjpmYWxzZSxcImZvcmtcIjpmYWxzZSxcImFyY2hpdmVkXCI6ZmFsc2UsXCJkZWZhdWx0X2JyYW5jaFwiOlwibWFpblwifSx7XCJpZFwiOjEwOTY3MTc5MTAsXCJuYW1lXCI6XCJMTE0tRnJvbS1TY3JhdGNoXCIsXCJmdWxsX25hbWVcIjpcIkFheWFucGF0ZWwwNS9MTE0tRnJvbS1TY3JhdGNoXCIsXCJkZXNjcmlwdGlvblwiOlwiQW4gZW5kLXRvLWVuZCBpbXBsZW1lbnRhdGlvbiBvZiBhIEdQVC1zdHlsZSBsYXJnZSBsYW5ndWFnZSBtb2RlbCBidWlsdCBmcm9tIHNjcmF0Y2ggdXNpbmcgUHl0aG9uIGFuZCBQeVRvcmNoLCBpbmNsdWRpbmcgdG9rZW5pemVyLCBhdHRlbnRpb24gbWVjaGFuaXNtLCB0cmFuc2Zvcm1lciBibG9ja3MsIHByZXRyYWluaW5nLCBhbmQgZmluZXR1bmluZy5cIixcImh0bWxfdXJsXCI6XCJodHRwczovL2dpdGh1Yi5jb20vQWF5YW5wYXRlbDA1L0xMTS1Gcm9tLVNjcmF0Y2hcIixcImxhbmd1YWdlXCI6XCJKdXB5dGVyIE5vdGVib29rXCIsXCJzdGFyZ2F6ZXJzX2NvdW50XCI6MCxcImZvcmtzX2NvdW50XCI6MCxcIm9wZW5faXNzdWVzX2NvdW50XCI6MCxcInVwZGF0ZWRfYXRcIjpcIjIwMjUtMTEtMTRUMjE6MjM6NDFaXCIsXCJjcmVhdGVkX2F0XCI6XCIyMDI1LTExLTE0VDIwOjU5OjUyWlwiLFwicHJpdmF0ZVwiOmZhbHNlLFwiZm9ya1wiOmZhbHNlLFwiYXJjaGl2ZWRcIjpmYWxzZSxcImRlZmF1bHRfYnJhbmNoXCI6XCJtYWluXCJ9LHtcImlkXCI6MTA4NjcxMTgwNixcIm5hbWVcIjpcIkRvY3VNaW5kLUxMTVwiLFwiZnVsbF9uYW1lXCI6XCJyYW1hcmF2L0RvY3VNaW5kLUxMTVwiLFwiZGVzY3JpcHRpb25cIjpcIiDwn6SWIENoYXQgd2l0aCB5b3VyIGRvY3VtZW50cyB1c2luZyBMYXJnZSBMYW5ndWFnZSBNb2RlbHMgKExMTXMpLCBGQUlTUywgYW5kIEh1Z2dpbmcgRmFjZSBUcmFuc2Zvcm1lcnMg4oCUIGFsbCBydW5uaW5nIGxvY2FsbHkgd2l0aCBTdHJlYW1saXQuXCIsXCJodG1sX3VybFwiOlwiaHR0cHM6Ly9naXRodWIuY29tL3JhbWFyYXYvRG9jdU1pbmQtTExNXCIsXCJsYW5ndWFnZVwiOlwiUHl0aG9uXCIsXCJzdGFyZ2F6ZXJzX2NvdW50XCI6MCxcImZvcmtzX2NvdW50XCI6MCxcIm9wZW5faXNzdWVzX2NvdW50XCI6MyxcInVwZGF0ZWRfYXRcIjpcIjIwMjUtMTEtMTRUMTM6Mzc6NTNaXCIsXCJjcmVhdGVkX2F0XCI6XCIyMDI1LTEwLTMwVDE5OjQ2OjQ1WlwiLFwidG9waWNzXCI6W1wiYWlcIixcImRlZXAtbGVhcm5pbmdcIixcImRvY3VtZW50LWFpXCIsXCJmYWlzc1wiLFwiZ2VuZXJhdGl2ZS1haVwiLFwiaHVnZ2luZy1mYWNlXCIsXCJsYW5nY2hhaW5cIixcImxsbVwiLFwibWFjaGluZS1sZWFybmluZ1wiLFwibmxwXCIsXCJwZGZcIixcInB5dGhvblwiLFwicW5hXCIsXCJyYWdcIixcInN0cmVhbWxpdFwiXSxcInByaXZhdGVcIjpmYWxzZSxcImZvcmtcIjpmYWxzZSxcImFyY2hpdmVkXCI6ZmFsc2UsXCJkZWZhdWx0X2JyYW5jaFwiOlwibWFpblwifSx7XCJpZFwiOjEwMjUzMTc5OTAsXCJuYW1lXCI6XCJ2YWxlcmllLmNcIixcImZ1bGxfbmFtZVwiOlwidGVsZXByaW50LW1lL3ZhbGVyaWUuY1wiLFwiZGVzY3JpcHRpb25cIjpcIkEgTGFyZ2UgTGFuZ3VhZ2UgTW9kZWwgd3JpdHRlbiBjb21wbGV0ZWx5IGZyb20gc2NyYXRjaCBpbiBwdXJlIEMuXCIsXCJodG1sX3VybFwiOlwiaHR0cHM6Ly9naXRodWIuY29tL3RlbGVwcmludC1tZS92YWxlcmllLmNcIixcImxhbmd1YWdlXCI6XCJDXCIsXCJzdGFyZ2F6ZXJzX2NvdW50XCI6MixcImZvcmtzX2NvdW50XCI6MCxcIm9wZW5faXNzdWVzX2NvdW50XCI6MCxcInVwZGF0ZWRfYXRcIjpcIjIwMjUtMTEtMTNUMjA6NDI6MjFaXCIsXCJjcmVhdGVkX2F0XCI6XCIyMDI1LTA3LTI0VDA0OjQ2OjE0WlwiLFwidG9waWNzXCI6W1wiY1wiLFwibGxtXCIsXCJ0cmFuc2Zvcm1lclwiXSxcInByaXZhdGVcIjpmYWxzZSxcImZvcmtcIjpmYWxzZSxcImFyY2hpdmVkXCI6ZmFsc2UsXCJkZWZhdWx0X2JyYW5jaFwiOlwibWFpblwifSx7XCJpZFwiOjEwODgxMTA2MTQsXCJuYW1lXCI6XCJMTE1fbWVtb3J5X2NvbnN0cmFpbnRzXCIsXCJmdWxsX25hbWVcIjpcIlNoYXlhblNoYWhyYWJpL0xMTV9tZW1vcnlfY29uc3RyYWludHNcIixcImRlc2NyaXB0aW9uXCI6XCJPZmZpY2lhbCByZXBvc2l0b3J5IG9mIFxcXCJDb2duaXRpdmUgRGl2ZXJzaXR5IGluIEFydGlmaWNpYWwgTWluZHMgLSBJbnZlc3RpZ2F0aW5nIHRoZSBQZXJmb3JtYW5jZSBvZiBMYXJnZSBMYW5ndWFnZSBNb2RlbHMgVW5kZXIgTWVtb3J5IENvbnN0cmFpbnRzXFxcIiAtIEhOQzIwMjVcIixcImh0bWxfdXJsXCI6XCJodHRwczovL2dpdGh1Yi5jb20vU2hheWFuU2hhaHJhYmkvTExNX21lbW9yeV9jb25zdHJhaW50c1wiLFwibGFuZ3VhZ2VcIjpcIlB5dGhvblwiLFwic3RhcmdhemVyc19jb3VudFwiOjAsXCJmb3Jrc19jb3VudFwiOjAsXCJvcGVuX2lzc3Vlc19jb3VudFwiOjAsXCJ1cGRhdGVkX2F0XCI6XCIyMDI1LTExLTEzVDEwOjU0OjQzWlwiLFwiY3JlYXRlZF9hdFwiOlwiMjAyNS0xMS0wMlQxMDozNjowMFpcIixcInRvcGljc1wiOltcImF0dGVudGlvbi1tZWNoYW5pc21cIixcImxsbVwiLFwibmV1cm9zY2llbmNlXCIsXCJ0cmFuc2Zvcm1lclwiXSxcInByaXZhdGVcIjpmYWxzZSxcImZvcmtcIjpmYWxzZSxcImFyY2hpdmVkXCI6ZmFsc2UsXCJkZWZhdWx0X2JyYW5jaFwiOlwibWFpblwifV19In1dfSx7InR5cGUiOiJtY3BfdG9vbF9yZXN1bHQiLCJ0b29sX3VzZV9pZCI6Im1jcHRvb2x1XzAxNHRKaWdOdjFLa0tMVER5ajZKOVdnNSIsImlzX2Vycm9yIjpmYWxzZSwiY29udGVudCI6W3sidHlwZSI6InRleHQiLCJ0ZXh0Ijoie1widG90YWxfY291bnRcIjoxMixcImluY29tcGxldGVfcmVzdWx0c1wiOmZhbHNlLFwiaXRlbXNcIjpbe1wiaWRcIjo5MDIwNTk0NjEsXCJuYW1lXCI6XCJMYW5nLU5BU1wiLFwiZnVsbF9uYW1lXCI6XCJreWVnb21lei9MYW5nLU5BU1wiLFwiZGVzY3JpcHRpb25cIjpcIlRoZSBQcm9tcHQgR3VpZGVkIE5ldXJhbCBBcmNoaXRlY3R1cmUgU2VhcmNoIChQRy1OQVMpIGlzIGEgUHlUb3JjaC1iYXNlZCBmcmFtZXdvcmsgZGVzaWduZWQgdG8gZ2VuZXJhdGUgbmV1cmFsIG5ldHdvcmsgYXJjaGl0ZWN0dXJlcyBiYXNlZCBvbiB0ZXh0dWFsIHByb21wdHMuXCIsXCJodG1sX3VybFwiOlwiaHR0cHM6Ly9naXRodWIuY29tL2t5ZWdvbWV6L0xhbmctTkFTXCIsXCJsYW5ndWFnZVwiOlwiUHl0aG9uXCIsXCJzdGFyZ2F6ZXJzX2NvdW50XCI6NCxcImZvcmtzX2NvdW50XCI6MCxcIm9wZW5faXNzdWVzX2NvdW50XCI6MSxcInVwZGF0ZWRfYXRcIjpcIjIwMjQtMTItMThUMTY6MzU6MzdaXCIsXCJjcmVhdGVkX2F0XCI6XCIyMDI0LTEyLTExVDIwOjM1OjE5WlwiLFwidG9waWNzXCI6W1wiYWdvcmFsYWJcIixcImFpXCIsXCJtZXRhXCIsXCJtbFwiLFwibmFzXCIsXCJuZXVyYWwtYXJjaGl0ZWN0dXJlLXNlYXJjaFwiLFwicHl0b3JjaFwiLFwicmVzZWFyY2hcIixcInRlbnNvcmZsb3dcIl0sXCJwcml2YXRlXCI6ZmFsc2UsXCJmb3JrXCI6ZmFsc2UsXCJhcmNoaXZlZFwiOmZhbHNlLFwiZGVmYXVsdF9icmFuY2hcIjpcIm1haW5cIn0se1wiaWRcIjo1NDU3NTc0MjQsXCJuYW1lXCI6XCJQbGFudC1sZWFmLWRpc2Vhc2VzLWRldGVjdGlvbi1cIixcImZ1bGxfbmFtZVwiOlwiYXNzYWxhYWJuay9QbGFudC1sZWFmLWRpc2Vhc2VzLWRldGVjdGlvbi1cIixcImRlc2NyaXB0aW9uXCI6XCJUaGUgYXV0b21hdGVkIGlkZW50aWZpY2F0aW9uIG9mIHBsYW50IGRpc2Vhc2VzIGJhc2VkIG9uIHBsYW50IGxlYXZlcyBpcyBhIGh1Z2UgYnJlYWt0aHJvdWdoLiBGdXJ0aGVybW9yZSwgZWFybHkgYW5kIGFjY3VyYXRlIGRldGVjdGlvbiBvZiBwbGFudCBkaXNlYXNlcyBwb3NpdGl2ZWx5IGltcGFjdHMgY3JvcCBwcm9kdWN0aXZpdHkgYW5kIHF1YWxpdHkuIEhvd2V2ZXIsIG1hbmFnaW5nIHRoZSBhY2Nlc3NpYmlsaXR5IG9mIGVhcmx5IHBsYW50IGRpc2Vhc2UgZGV0ZWN0aW9uIGlzIGNydWNpYWwuIFRoaXMgd29yayBoYXMgZW52aXJvbm1lbnRhbCBnb2FscyBhaW1pbmcgdG8gc2F2ZSBwbGFudHMgZnJvbSBkaWZmZXJlbnQgdGhyZWF0ZW5pbmcgZGlzZWFzZXMgYnkgcHJvdmlkaW5nIGVhcmx5IGRldGVjdGlvbiBvZiB0aGUgYWZmZWN0ZWQgbGVhdmVzLiBXZSBzdHVkaWVkIHRoZSBwZXJmb3JtYW5jZSBvZiBkaWZmZXJlbnQgQ29udm9sdXRpb25hbCBOZXVyYWwgTmV0d29yayAoQ05OKSBhcmNoaXRlY3R1cmVzIGluIHByZWRpY3RpbmcgMjYgZGlzZWFzZXMgZm9yIDE0IHBsYW50IHNwZWNpZXMuIFRoZSB3b3JrIHN0dWRpZWQgdGhlIGNvbXBsZXhpdHkgb2YgdGhlIHN5c3RlbSBhbmQgY29tcGFyZWQgdGhlIHR3byBtYWluIGRlZXAgbGVhcm5pbmcgZnJhbWV3b3JrcywgVGVuc29yRmxvdyBhbmQgUHlUb3JjaCwgdG8gZ2V0IHRoZSBtb3N0IGFjY3VyYXRlIHJlc3VsdHMgd2l0aCBoaWdoZXIgYWNjdXJhY3kuIFVzaW5nIHRoZSBcXHUwMDI2cXVvdDtOZXcgUGxhbnRWaWxsYWdlIERhdGFzZXRcXHUwMDI2cXVvdDsgZnJvbSBLYWdnbGUgWzFdLCB0aGUgVGVuc29yRmxvdyBtb2RlbHMgYWNoaWV2ZWQgYW4gYWNjdXJhY3kgb2YgOTAsOTQlIGZvciB0aGUgYmFzaWMgQ0NOIGFyY2hpdGVjdHVyZSwgYW5kIDk1LDU5JSBmb3IgdGhlIFRyYW5zZmVyIExlYXJuaW5nIGFyY2hpdGVjdHVyZSB3aXRoIFZHRzE5LiBXaGVyZWFzIHRoZSBQeVRvcmNoIG1vZGVscyBhY2hpZXZlZCBhbiBhY2N1cmFjeSBvZiA5Myw0NyUgZm9yIHRoZSBiYXNpYyBDQ04gYXJjaGl0ZWN0dXJlLCBhbmQgOTgsNTMlIGZvciB0aGUgVHJhbnNmZXIgTGVhcm5pbmcgYXJjaGl0ZWN0dXJlIHdpdGggUmVzTmV0MzQuIEZpbmFsbHksIGFmdGVyIGV4YW1pbmluZyB0aGUgZmVhc2liaWxpdHkgb2YgdGhlIG1vZGVsXFx1MDAyNiMzOTtzIGltcGxlbWVudGF0aW9uIGFuZCBkaXNjdXNzaW5nIHRoZSBtYWluIHByb2JsZW1zIHRoYXQgbWF5IGJlIGVuY291bnRlcmVkLCB0aGUgbW9kZWxzIHdlcmUgZGVwbG95ZWQgaW4gYSBtb2JpbGUgYXBwbGljYXRpb24gdXNpbmcgdGhlIFRmbGl0ZSBhbmQgdG9yY2ggbW9iaWxlIGZsdXR0ZXIgU0RLIHRvIGxldCB0aGVtIGFzIGFuIGludGVybmFsIGZlYXR1cmUgaW4gdGhlIG1vYmlsZSB3aXRob3V0IHRoZSBuZWVkIGZvciBhbnkgYWNjZXNzIHRvIHRoZSBjbG91ZCwgd2hpY2ggaXMga25vd24gYXMgZWRnZSBBSS5cIixcImh0bWxfdXJsXCI6XCJodHRwczovL2dpdGh1Yi5jb20vYXNzYWxhYWJuay9QbGFudC1sZWFmLWRpc2Vhc2VzLWRldGVjdGlvbi1cIixcImxhbmd1YWdlXCI6XCJEYXJ0XCIsXCJzdGFyZ2F6ZXJzX2NvdW50XCI6MyxcImZvcmtzX2NvdW50XCI6MCxcIm9wZW5faXNzdWVzX2NvdW50XCI6MSxcInVwZGF0ZWRfYXRcIjpcIjIwMjQtMTEtMjNUMDk6MTg6MThaXCIsXCJjcmVhdGVkX2F0XCI6XCIyMDIyLTEwLTA0VDIzOjM3OjE1WlwiLFwicHJpdmF0ZVwiOmZhbHNlLFwiZm9ya1wiOmZhbHNlLFwiYXJjaGl2ZWRcIjpmYWxzZSxcImRlZmF1bHRfYnJhbmNoXCI6XCJtYWluXCJ9LHtcImlkXCI6OTc3NjkwOTE3LFwibmFtZVwiOlwiVmFuaWxsQUlcIixcImZ1bGxfbmFtZVwiOlwiYmF0dWhhbmVyYWxwL1ZhbmlsbEFJXCIsXCJkZXNjcmlwdGlvblwiOlwiVmFuaWxsQUkgaXMgYSBtaW5pbWFsaXN0IE1ML0RMIGZyYW1ld29yayBidWlsdCB3aXRoIDEwMCUgdmFuaWxsYSBQeXRob24g4oCTIG5vIE51bVB5LCBubyBQeVRvcmNoLCBubyBUZW5zb3JGbG93LiBUaGlzIHByb2plY3QgaXMgZm9yIHRob3NlIHdobyB3YW50IHRvIGxlYXJuLCB0ZWFjaCwgb3IgZGVlcGx5IHVuZGVyc3RhbmQgdGhlIG1lY2hhbmljcyBvZiBuZXVyYWwgbmV0d29ya3MgYW5kIG9wdGltaXphdGlvbiBhbGdvcml0aG1zIGZyb20gdGhlIGdyb3VuZCB1cC4gUmVxdWlyZW1lbnRzPyBQeXRob24uXCIsXCJodG1sX3VybFwiOlwiaHR0cHM6Ly9naXRodWIuY29tL2JhdHVoYW5lcmFscC9WYW5pbGxBSVwiLFwibGFuZ3VhZ2VcIjpcIlB5dGhvblwiLFwic3RhcmdhemVyc19jb3VudFwiOjEsXCJmb3Jrc19jb3VudFwiOjAsXCJvcGVuX2lzc3Vlc19jb3VudFwiOjAsXCJ1cGRhdGVkX2F0XCI6XCIyMDI1LTA1LTA2VDIyOjAzOjExWlwiLFwiY3JlYXRlZF9hdFwiOlwiMjAyNS0wNS0wNFQxOTowNDozMVpcIixcInByaXZhdGVcIjpmYWxzZSxcImZvcmtcIjpmYWxzZSxcImFyY2hpdmVkXCI6ZmFsc2UsXCJkZWZhdWx0X2JyYW5jaFwiOlwibWFpblwifSx7XCJpZFwiOjUzNzE5NjU3MixcIm5hbWVcIjpcIlN5bWJsLmFpLUJ1aWxkaW5nLU5ldXJhbC1OZXR3b3JrLVRlbnNvckZsb3ctUHlUb3JjaFwiLFwiZnVsbF9uYW1lXCI6XCJjb250ZW50bGFiLWlvL1N5bWJsLmFpLUJ1aWxkaW5nLU5ldXJhbC1OZXR3b3JrLVRlbnNvckZsb3ctUHlUb3JjaFwiLFwiZGVzY3JpcHRpb25cIjpcIkxlYXJuIHRvIGNyZWF0ZSBhbmQgdHJhaW4gYSBuZXVyYWwgbmV0d29yayBmb3IgdGhlIG11bHRpLWNsYXNzIGNsYXNzaWZpY2F0aW9uIHVzaW5nIFRlbnNvckZsb3cgYW5kIFB5VG9yY2guIERpc2NvdmVyIHdoaWNoIGZyYW1ld29yayBpcyBzdWl0YWJsZSBmb3IgYmVnaW5uZXJzLlwiLFwiaHRtbF91cmxcIjpcImh0dHBzOi8vZ2l0aHViLmNvbS9jb250ZW50bGFiLWlvL1N5bWJsLmFpLUJ1aWxkaW5nLU5ldXJhbC1OZXR3b3JrLVRlbnNvckZsb3ctUHlUb3JjaFwiLFwibGFuZ3VhZ2VcIjpcIkp1cHl0ZXIgTm90ZWJvb2tcIixcInN0YXJnYXplcnNfY291bnRcIjowLFwiZm9ya3NfY291bnRcIjowLFwib3Blbl9pc3N1ZXNfY291bnRcIjowLFwidXBkYXRlZF9hdFwiOlwiMjAyMi0wOS0xNVQyMDo0OToxNlpcIixcImNyZWF0ZWRfYXRcIjpcIjIwMjItMDktMTVUMjA6MzA6MzZaXCIsXCJwcml2YXRlXCI6ZmFsc2UsXCJmb3JrXCI6ZmFsc2UsXCJhcmNoaXZlZFwiOmZhbHNlLFwiZGVmYXVsdF9icmFuY2hcIjpcIm1haW5cIn0se1wiaWRcIjo2NjUyODkzMTMsXCJuYW1lXCI6XCJGQUEtQWlybWFuLVF1YWxpZmljYXRpb24tUHJlZGljdGlvblwiLFwiZnVsbF9uYW1lXCI6XCJiaW5zMDAwMC9GQUEtQWlybWFuLVF1YWxpZmljYXRpb24tUHJlZGljdGlvblwiLFwiZGVzY3JpcHRpb25cIjpcIkVtcGxveWVkIGN1dHRpbmctZWRnZSBNYWNoaW5lIExlYXJuaW5nIGFuZCBEZWVwIExlYXJuaW5nIGFsZ29yaXRobXMsIGluY2x1ZGluZyBTVk0sIFJhbmRvbSBGb3Jlc3QsIGFuZCBOZXVyYWwgTmV0d29ya3MsIHRvIGRldmVsb3AgYSBkeW5hbWljIGZyYW1ld29yayBmb3IgYXNzZXNzaW5nIHBpbG90cycgcXVhbGlmaWNhdGlvbiB1c2luZyBQeXRob24gYW5kIHByb21pbmVudCBNTCBsaWJyYXJpZXMgc3VjaCBhcyBzY2lraXQtbGVhcm4sIFRlbnNvckZsb3csIGFuZCBQeVRvcmNoLlwiLFwiaHRtbF91cmxcIjpcImh0dHBzOi8vZ2l0aHViLmNvbS9iaW5zMDAwMC9GQUEtQWlybWFuLVF1YWxpZmljYXRpb24tUHJlZGljdGlvblwiLFwibGFuZ3VhZ2VcIjpcIkp1cHl0ZXIgTm90ZWJvb2tcIixcInN0YXJnYXplcnNfY291bnRcIjowLFwiZm9ya3NfY291bnRcIjowLFwib3Blbl9pc3N1ZXNfY291bnRcIjowLFwidXBkYXRlZF9hdFwiOlwiMjAyMy0wNy0xMlQwMTozOToyOVpcIixcImNyZWF0ZWRfYXRcIjpcIjIwMjMtMDctMTFUMjE6NTM6MTRaXCIsXCJ0b3BpY3NcIjpbXCJiaWctZGF0YVwiLFwiZGVlcC1sZWFybmluZ1wiLFwiaGVhbHRoY2FyZS1kYXRhXCIsXCJtYWNoaW5lLWxlYXJuaW5nXCIsXCJuZXVyYWwtbmV0d29ya1wiLFwicHl0b3JjaFwiLFwic2tsZWFyblwiXSxcInByaXZhdGVcIjpmYWxzZSxcImZvcmtcIjpmYWxzZSxcImFyY2hpdmVkXCI6ZmFsc2UsXCJkZWZhdWx0X2JyYW5jaFwiOlwibWFpblwifSx7XCJpZFwiOjc4MDQzOTI0OCxcIm5hbWVcIjpcIkRlZXAtTGVhcm5pbmctcHJvamVjdHNcIixcImZ1bGxfbmFtZVwiOlwiZG9yaWFuRHJhcGVyL0RlZXAtTGVhcm5pbmctcHJvamVjdHNcIixcImRlc2NyaXB0aW9uXCI6XCJXZSBlbWJhcmsgb24gYSBqb3VybmV5IHRocm91Z2ggdmFyaW91cyBkZWVwIGxlYXJuaW5nIHByb2plY3RzLCBkZWx2aW5nIGludG8gZGl2ZXJzZSBtb2RlbHMsIHRlY2huaXF1ZXMsIG1ldGhvZHMsIGFuZCBmcmFtZXdvcmtzLiBGcm9tIGNvbnZvbHV0aW9uYWwgbmV1cmFsIG5ldHdvcmtzIChDTk5zKSB0byByZWN1cnJlbnQgbmV1cmFsIG5ldHdvcmtzIChSTk5zKSwgZnJvbSBUZW5zb3JGbG93IHRvIFB5VG9yY2gsIHdlIHVuY292ZXIgdGhlIGludHJpY2FjaWVzIG9mIGN1dHRpbmctZWRnZSBBSSB0ZWNobm9sb2dpZXMuIFwiLFwiaHRtbF91cmxcIjpcImh0dHBzOi8vZ2l0aHViLmNvbS9kb3JpYW5EcmFwZXIvRGVlcC1MZWFybmluZy1wcm9qZWN0c1wiLFwibGFuZ3VhZ2VcIjpcIkp1cHl0ZXIgTm90ZWJvb2tcIixcInN0YXJnYXplcnNfY291bnRcIjowLFwiZm9ya3NfY291bnRcIjowLFwib3Blbl9pc3N1ZXNfY291bnRcIjowLFwidXBkYXRlZF9hdFwiOlwiMjAyNC0wNi0wN1QxNDoxODozMVpcIixcImNyZWF0ZWRfYXRcIjpcIjIwMjQtMDQtMDFUMTM6NDA6NDdaXCIsXCJwcml2YXRlXCI6ZmFsc2UsXCJmb3JrXCI6ZmFsc2UsXCJhcmNoaXZlZFwiOmZhbHNlLFwiZGVmYXVsdF9icmFuY2hcIjpcIm1haW5cIn0se1wiaWRcIjo2NTM1OTYxMTMsXCJuYW1lXCI6XCJkc2xcIixcImZ1bGxfbmFtZVwiOlwiTmV1cmFsRmxvZ28vZHNsXCIsXCJkZXNjcmlwdGlvblwiOlwiRmxvZ28gaXMgYSBEb21haW4tU3BlY2lmaWMgTGFuZ3VhZ2UgKERTTCkgZGVzaWduZWQgdG8gc2ltcGxpZnkgYW5kIGF1dG9tYXRlIHRoZSBjcmVhdGlvbiBvZiBuZXVyYWwgbmV0d29ya3MuIEl0IGFpbXMgdG8gYWJzdHJhY3QgdGhlIHRlY2huaWNhbCBjb21wbGV4aXR5IG9mIGZyYW1ld29ya3MgbGlrZSBQeVRvcmNoIG9yIFRlbnNvckZsb3csIGFsbG93aW5nIGRldmVsb3BlcnMgdG8gbW9kZWwgYm90aCB0aGUgc3RydWN0dXJlIG9mIG5ldXJhbCBuZXR3b3JrcyBhbmQgdGhlaXIgdHJhaW5pbmcgcHJvY2VzcyB1c2luZyBhIG1vcmUgaW50dWl0aXZlIGFuZCBzdHJhaWdodGZvcndhcmQgbGFuZ3VhZ2UuXCIsXCJodG1sX3VybFwiOlwiaHR0cHM6Ly9naXRodWIuY29tL05ldXJhbEZsb2dvL2RzbFwiLFwibGFuZ3VhZ2VcIjpcIkphdmFcIixcInN0YXJnYXplcnNfY291bnRcIjowLFwiZm9ya3NfY291bnRcIjoxLFwib3Blbl9pc3N1ZXNfY291bnRcIjowLFwidXBkYXRlZF9hdFwiOlwiMjAyNC0wNi0xMFQwNjo1NjozOVpcIixcImNyZWF0ZWRfYXRcIjpcIjIwMjMtMDYtMTRUMTE6MDA6MzFaXCIsXCJ0b3BpY3NcIjpbXCJhaVwiLFwiYXJ0aWZpY2lhbC1pbnRlbGxpZ2VuY2VcIixcImRlZXAtbGVhcm5pbmdcIixcImRzbFwiLFwiaW50dWl0aXZlXCIsXCJuZXVyYWwtbmV0d29ya1wiLFwic2ltcGxlXCJdLFwicHJpdmF0ZVwiOmZhbHNlLFwiZm9ya1wiOmZhbHNlLFwiYXJjaGl2ZWRcIjpmYWxzZSxcImRlZmF1bHRfYnJhbmNoXCI6XCJtYXN0ZXJcIn0se1wiaWRcIjo5MjYwNjExNDcsXCJuYW1lXCI6XCIuLUFJLVBvd2VyZWQtSW1hZ2UtQ29sb3JpemF0aW9uLVNvbHV0aW9uXCIsXCJmdWxsX25hbWVcIjpcInByaXlhbmthY2hhdWRoYXJpMTAxLy4tQUktUG93ZXJlZC1JbWFnZS1Db2xvcml6YXRpb24tU29sdXRpb25cIixcImRlc2NyaXB0aW9uXCI6XCJBbiBBSS1wb3dlcmVkIHNvbHV0aW9uIHRoYXQgY29sb3JpemVzIGdyYXlzY2FsZSBpbWFnZXMgdXNpbmcgZGVlcCBuZXVyYWwgbmV0d29ya3MsIGluc3BpcmVkIGJ5IEVDQ1YgMjAxNiBcXHUwMDI2IFNJR0dSQVBIIDIwMTcuIEl0IHN1cHBvcnRzIGNvbnRlbnQgcmVzdG9yYXRpb24sIGUtY29tbWVyY2UsIGFuZCBjcmVhdGl2ZSB3b3JrZmxvd3MsIGVuc3VyaW5nIGhpZ2gtcXVhbGl0eSwgdmlicmFudCByZXN1bHRzLiBPcmlnaW5hbGx5IGltcGxlbWVudGVkIGluIENhZmZlLCBpdCdzIHNjYWxhYmxlIGFuZCBhZGFwdGFibGUgdG8gbW9kZXJuIGZyYW1ld29ya3MgbGlrZSBUZW5zb3JGbG93L1B5VG9yY2hcIixcImh0bWxfdXJsXCI6XCJodHRwczovL2dpdGh1Yi5jb20vcHJpeWFua2FjaGF1ZGhhcmkxMDEvLi1BSS1Qb3dlcmVkLUltYWdlLUNvbG9yaXphdGlvbi1Tb2x1dGlvblwiLFwic3RhcmdhemVyc19jb3VudFwiOjAsXCJmb3Jrc19jb3VudFwiOjAsXCJvcGVuX2lzc3Vlc19jb3VudFwiOjAsXCJ1cGRhdGVkX2F0XCI6XCIyMDI1LTAyLTAyVDEzOjEwOjA4WlwiLFwiY3JlYXRlZF9hdFwiOlwiMjAyNS0wMi0wMlQxMzoxMDowNFpcIixcInByaXZhdGVcIjpmYWxzZSxcImZvcmtcIjpmYWxzZSxcImFyY2hpdmVkXCI6ZmFsc2UsXCJkZWZhdWx0X2JyYW5jaFwiOlwibWFpblwifSx7XCJpZFwiOjI3MzQyMTE2OCxcIm5hbWVcIjpcImNhbUFJcmFcIixcImZ1bGxfbmFtZVwiOlwid2lkaWFzYW1vc2lyL2NhbUFJcmFcIixcImRlc2NyaXB0aW9uXCI6XCJBcHBsaWNhdGlvbiBvZiBEZWVwIExlYXJuaW5nIE5ldHdvcmsgZm9yIEVuaGFuY2UgTG93IFJlc29sdXRpb24gaW50byBIaWdoIFJlc29sdXRpb24gYW5kIFJlc3RvcmluZyBEYXJrIEltYWdlcyBpbnRvIEJyaWdodCBhbmQgQ2xlYXIgSW1hZ2UuIFNpbmdsZSBJbWFnZSBTdXBlci1SZXNvbHV0aW9uIHVzaW5nIEVTUkdBTiBhcyBuZXVyYWwgbmV0d29yayB0byBlbmhhbmNlIExvdy1SZXMgaW1hZ2UgaW50byBIaWdoLXJlcyBJbWFnZS4gVXNpbmcgRkNOTiBhcyBhIG5ldXJhbCBuZXR3b3JrIGZvciByZXN0b3JpbmcgZGFyayBpbWFnZXMuIERlZXAgbGVhcm5pbmcgdXNpbmcgVGVuc29yZmxvdywgUHl0b3JjaCwgYW5kIElTUiBtb2R1bGUgYXMgYSBmcmFtZXdvcmsgbW9kZWwuIEZvciBkZXBsb3lpbmcgbW9kZWwgdG8gd2Vic2l0ZSwgdXNpbmcgRmxhc2sgYW5kIEZsYXNrIE5ncm9rLiBOYW1lbHkgY2FtQUlyYSBhcyBJbWFnZSBQcm9jZXNzaW5nIHVzaW5nIEFydGlmaWNpYWwgSW50ZWxsaWdlbmNlIFRlY2hub2xvZ3kuXCIsXCJodG1sX3VybFwiOlwiaHR0cHM6Ly9naXRodWIuY29tL3dpZGlhc2Ftb3Npci9jYW1BSXJhXCIsXCJsYW5ndWFnZVwiOlwiUHl0aG9uXCIsXCJzdGFyZ2F6ZXJzX2NvdW50XCI6MCxcImZvcmtzX2NvdW50XCI6MCxcIm9wZW5faXNzdWVzX2NvdW50XCI6MCxcInVwZGF0ZWRfYXRcIjpcIjIwMjItMDEtMjlUMTU6NDU6MzlaXCIsXCJjcmVhdGVkX2F0XCI6XCIyMDIwLTA2LTE5VDA2OjI5OjI1WlwiLFwicHJpdmF0ZVwiOmZhbHNlLFwiZm9ya1wiOmZhbHNlLFwiYXJjaGl2ZWRcIjpmYWxzZSxcImRlZmF1bHRfYnJhbmNoXCI6XCJtYXN0ZXJcIn0se1wiaWRcIjo5NzA2OTQ1NzEsXCJuYW1lXCI6XCJEZWVwLUxlYXJuaW5nXCIsXCJmdWxsX25hbWVcIjpcImlqYXlzbGF5L0RlZXAtTGVhcm5pbmdcIixcImRlc2NyaXB0aW9uXCI6XCJBIGNvbGxlY3Rpb24gb2YgRGVlcCBMZWFybmluZyBwcmFjdGljYWxzIGltcGxlbWVudGVkIGFuZCBwcmFjdGljZWQgZHVyaW5nIGNsYXNzIHNlc3Npb25zLiBUaGlzIHJlcG9zaXRvcnkgaXMgaW50ZW5kZWQgZm9yIHN0dWRlbnRzIGFuZCBiZWdpbm5lcnMgdG8gdW5kZXJzdGFuZCB0aGUgYmFzaWNzIG9mIG5ldXJhbCBuZXR3b3JrcywgdHJhaW5pbmcgcGlwZWxpbmVzLCBhbmQgcmVhbC13b3JsZCBBSSBhcHBsaWNhdGlvbnMgdXNpbmcgZnJhbWV3b3JrcyBsaWtlIFRlbnNvckZsb3cgYW5kIFB5VG9yY2hcIixcImh0bWxfdXJsXCI6XCJodHRwczovL2dpdGh1Yi5jb20vaWpheXNsYXkvRGVlcC1MZWFybmluZ1wiLFwibGFuZ3VhZ2VcIjpcIkp1cHl0ZXIgTm90ZWJvb2tcIixcInN0YXJnYXplcnNfY291bnRcIjowLFwiZm9ya3NfY291bnRcIjowLFwib3Blbl9pc3N1ZXNfY291bnRcIjowLFwidXBkYXRlZF9hdFwiOlwiMjAyNS0wOC0zMVQyMDoxMzo1MVpcIixcImNyZWF0ZWRfYXRcIjpcIjIwMjUtMDQtMjJUMTE6NTU6MTBaXCIsXCJwcml2YXRlXCI6ZmFsc2UsXCJmb3JrXCI6ZmFsc2UsXCJhcmNoaXZlZFwiOmZhbHNlLFwiZGVmYXVsdF9icmFuY2hcIjpcIm1haW5cIn0se1wiaWRcIjo3MTkxNzEzOTQsXCJuYW1lXCI6XCJNTFByb2plY3RzXCIsXCJmdWxsX25hbWVcIjpcIkNoaWRlcmFCZWwvTUxQcm9qZWN0c1wiLFwiZGVzY3JpcHRpb25cIjpcIk1MIHByb2plY3RzIHdvcmtlZCBvbiB3aGlsZSBsZWFybmluZyBuZXcgbWFjaGluZSBsZWFybmluZyBjb25jZXB0cyBhbmQgQUkgZW5naW5lZXJpbmcuIEluY2x1ZGVzIGxpbmVhciwgbXVsdGktbGluZWFyIFxcdTAwMjYgbG9naXN0aWMgcmVncmVzc2lvbiBtb2RlbHMgYXMgd2VsbCBhcyBjbGFzc2lmaWNhdGlvbiBtb2RlbHMuIEFkZGl0aW9uYWxseSwgbGVhcm5pbmcgY29uY2VwdHMgb2YgY2xhc3NpZmljYXRpb24gYW5kIHJlZ3Jlc3Npb24gdXNpbmcgZGVlcCBsZWFybmluZyBuZXVyYWwgbmV0d29ya3MuIEZyYW1ld29ya3MgdXNlZDogUHlUb3JjaCwgS2VyYXMsIFRlbnNvckZsb3csIFBhbmRhcywgTnVtcHksIGUudC5jXCIsXCJodG1sX3VybFwiOlwiaHR0cHM6Ly9naXRodWIuY29tL0NoaWRlcmFCZWwvTUxQcm9qZWN0c1wiLFwibGFuZ3VhZ2VcIjpcIkp1cHl0ZXIgTm90ZWJvb2tcIixcInN0YXJnYXplcnNfY291bnRcIjowLFwiZm9ya3NfY291bnRcIjowLFwib3Blbl9pc3N1ZXNfY291bnRcIjowLFwidXBkYXRlZF9hdFwiOlwiMjAyNC0wMS0wNlQwOTo1OToxMlpcIixcImNyZWF0ZWRfYXRcIjpcIjIwMjMtMTEtMTVUMTU6NTE6NDFaXCIsXCJwcml2YXRlXCI6ZmFsc2UsXCJmb3JrXCI6ZmFsc2UsXCJhcmNoaXZlZFwiOmZhbHNlLFwiZGVmYXVsdF9icmFuY2hcIjpcIm1hc3RlclwifSx7XCJpZFwiOjk5MDg3ODc2NixcIm5hbWVcIjpcIkRlZXAtbGVhcm5pbmdcIixcImZ1bGxfbmFtZVwiOlwiTVVIQU1NQURLQVNISUYzMzIyL0RlZXAtbGVhcm5pbmdcIixcImRlc2NyaXB0aW9uXCI6XCJFeHBsb3JpbmcgYWR2YW5jZWQgZGVlcCBsZWFybmluZyB0ZWNobmlxdWVzIGFuZCBvcHRpbWl6aW5nIG5ldXJhbCBuZXR3b3JrIGFyY2hpdGVjdHVyZXMhIFBhc3Npb25hdGUgYWJvdXQgbW9kZWwgZWZmaWNpZW5jeSwgaHlwZXJwYXJhbWV0ZXIgdHVuaW5nLCBhbmQgbGV2ZXJhZ2luZyBmcmFtZXdvcmtzIGxpa2UgVGVuc29yRmxvdyBhbmQgUHlUb3JjaCBmb3Igc2NhbGFibGUgQUkgc29sdXRpb25zLiBBbHdheXMgZWFnZXIgdG8gcmVmaW5lIHdvcmtmbG93cyBhbmQgcHVzaCB0aGUgYm91bmRhcmllcyBvZiBwZXJmb3JtYW5jZS4gTGV0IG1lIGtub3cgaWYgeW914oCZZCBsaWtlIGFueSBhZGp1c3RtZW50cyEg8J+agFwiLFwiaHRtbF91cmxcIjpcImh0dHBzOi8vZ2l0aHViLmNvbS9NVUhBTU1BREtBU0hJRjMzMjIvRGVlcC1sZWFybmluZ1wiLFwic3RhcmdhemVyc19jb3VudFwiOjAsXCJmb3Jrc19jb3VudFwiOjAsXCJvcGVuX2lzc3Vlc19jb3VudFwiOjAsXCJ1cGRhdGVkX2F0XCI6XCIyMDI1LTA1LTMwVDE4OjA3OjExWlwiLFwiY3JlYXRlZF9hdFwiOlwiMjAyNS0wNS0yNlQxOTozMDoxM1pcIixcInByaXZhdGVcIjpmYWxzZSxcImZvcmtcIjpmYWxzZSxcImFyY2hpdmVkXCI6ZmFsc2UsXCJkZWZhdWx0X2JyYW5jaFwiOlwibWFpblwifV19In1dfSx7InR5cGUiOiJ0ZXh0IiwidGV4dCI6IkJhc2VkIG9uIG15IHJlc2VhcmNoIG9mIEFJIGRldmVsb3BtZW50cyBvbiBHaXRIdWIsIGhlcmUncyBhIGNvbXByZWhlbnNpdmUgb3ZlcnZpZXcgb2YgY3VycmVudCB0cmVuZHMgYW5kIGtleSBwcm9qZWN0czpcblxuIyMgKipUb3AgQUkgRGV2ZWxvcG1lbnQgVHJlbmRzIG9uIEdpdEh1YioqXG5cbiMjIyAqKjEuIExhcmdlIExhbmd1YWdlIE1vZGVscyAoTExNcykgLSBUaGUgRG9taW5hbnQgRm9yY2UqKlxuXG4qKktleSBQcm9qZWN0czoqKlxuLSAqKlNHTGFuZyoqICgyMC4yayBzdGFycykgLSBGYXN0IHNlcnZpbmcgZnJhbWV3b3JrIGZvciBMTE1zIGFuZCB2aXNpb24gbGFuZ3VhZ2UgbW9kZWxzXG4gIC0gU3VwcG9ydHM6IERlZXBTZWVrLCBHUFQsIExsYW1hLCBMbGF2YSwgYW5kIG90aGVyIGN1dHRpbmctZWRnZSBtb2RlbHNcbiAgLSBGb2N1cyBvbiBpbmZlcmVuY2Ugb3B0aW1pemF0aW9uIGFuZCBtb2RlbCBkZXBsb3ltZW50XG5cbi0gKipMTE1zIGZyb20gU2NyYXRjaCoqIC0gRWR1Y2F0aW9uYWwgcHJvamVjdHMgdGVhY2hpbmcgaG93IHRvIGJ1aWxkIEdQVC1saWtlIG1vZGVsc1xuICAtIENvdmVycyB0cmFuc2Zvcm1lcnMsIGF0dGVudGlvbiBtZWNoYW5pc21zLCBhbmQgdG9rZW5pemF0aW9uXG4gIC0gQnVpbHQgd2l0aCBQeVRvcmNoIGZyb20gZ3JvdW5kIHVwXG5cbiMjIyAqKjIuIE1ML0RMIEZyYW1ld29ya3MgJiBUb29scyoqXG5cbioqUHlUb3JjaCB2cyBUZW5zb3JGbG93OioqIEJvdGggcmVtYWluIGRvbWluYW50LCB3aXRoIFB5VG9yY2ggZ2FpbmluZyB0cmFjdGlvbiBpbiByZXNlYXJjaDpcbi0gKipQeVRvcmNoKio6IFByZWZlcnJlZCBmb3IgcmVzZWFyY2ggYW5kIGN1c3RvbSBhcmNoaXRlY3R1cmVzXG4tICoqVGVuc29yRmxvdyoqOiBTdHJvbmcgaW4gcHJvZHVjdGlvbiBkZXBsb3ltZW50cyBhbmQgZWRnZSBjb21wdXRpbmdcblxuKipFbWVyZ2luZyBUb29sczoqKlxuLSBTR0xhbmcgKGluZmVyZW5jZSBvcHRpbWl6YXRpb24pXG4tIFRyYW5zZm9ybWVyTGFiIEFwcCAoNC41ayBzdGFycykgLSBPcGVuLXNvdXJjZSBhcHAgZm9yIExMTSBlbmdpbmVlcmluZyB3aXRoIGZpbmUtdHVuaW5nIGNhcGFiaWxpdGllc1xuXG4jIyMgKiozLiBLZXkgQUkgRG9tYWlucyoqXG5cbioqTmV1cmFsIEFyY2hpdGVjdHVyZSBTZWFyY2ggKE5BUyk6Kipcbi0gUHJvamVjdHMgdXNpbmcgcHJvbXB0LWd1aWRlZCBhcmNoaXRlY3R1cmUgZ2VuZXJhdGlvblxuLSBBdXRvbWF0aW5nIG1vZGVsIGRlc2lnbiB1c2luZyBuYXR1cmFsIGxhbmd1YWdlXG5cbioqQ29tcHV0ZXIgVmlzaW9uOioqXG4tIEltYWdlIGNsYXNzaWZpY2F0aW9uIHdpdGggQ05Oc1xuLSBPYmplY3QgZGV0ZWN0aW9uIHdpdGggWU9MT3YzXG4tIEltYWdlIHN1cGVyLXJlc29sdXRpb24gYW5kIHJlc3RvcmF0aW9uXG4tIFBsYW50IGRpc2Vhc2UgZGV0ZWN0aW9uIHN5c3RlbXNcblxuKipOYXR1cmFsIExhbmd1YWdlIFByb2Nlc3Npbmc6Kipcbi0gVHJhbnNmb3JtZXIgaW1wbGVtZW50YXRpb25zXG4tIFRva2VuIHByb2Nlc3NpbmcgYW5kIGVtYmVkZGluZ3Ncbi0gQ2hhdCBzeXN0ZW1zIGFuZCBRJkEgbW9kZWxzXG5cbiMjIyAqKjQuIFByYWN0aWNhbCBBcHBsaWNhdGlvbnMqKlxuXG4tICoqSGVhbHRoY2FyZSBBSSoqOiBNZWRpY2FsIGltYWdlIGFuYWx5c2lzLCBkaWFnbm9zdGljIHN5c3RlbXNcbi0gKipBZ3JpY3VsdHVyZSoqOiBDcm9wIGRpc2Vhc2UgZGV0ZWN0aW9uIHVzaW5nIGRlZXAgbGVhcm5pbmdcbi0gKipFLWNvbW1lcmNlKio6IFByb2R1Y3QgcmVjb21tZW5kYXRpb24gZW5naW5lc1xuLSAqKkRvY3VtZW50IEFJKio6IFJBRyAoUmV0cmlldmFsLUF1Z21lbnRlZCBHZW5lcmF0aW9uKSBzeXN0ZW1zXG4tICoqRWRnZSBBSSoqOiBNb2JpbGUgZGVwbG95bWVudCB3aXRoIFRlbnNvckZsb3cgTGl0ZSBhbmQgUHlUb3JjaCBNb2JpbGVcblxuIyMjICoqNS4gTGVhcm5pbmcgUmVzb3VyY2VzKipcblxuUG9wdWxhciByZXBvc2l0b3JpZXMgaW5jbHVkZTpcbi0gKio1MDArIEFJL01MIFByb2plY3RzKiogKDI4LjhrIHN0YXJzKSAtIENvbXByZWhlbnNpdmUgY29sbGVjdGlvbiB3aXRoIGNvZGVcbi0gKipEZWVwIExlYXJuaW5nIFNwZWNpYWxpemF0aW9uKiogKDRrIHN0YXJzKSAtIENvdXJzZSBtYXRlcmlhbHMgYW5kIGFzc2lnbm1lbnRzXG4tICoqTmV0cm9uKiogKDMxLjhrIHN0YXJzKSAtIE5ldXJhbCBuZXR3b3JrIHZpc3VhbGl6YXRpb24gdG9vbFxuLSAqKkNoZWF0c2hlZXRzIGZvciBBSSoqICgxNS4zayBzdGFycykgLSBRdWljayByZWZlcmVuY2UgZ3VpZGVzXG5cbiMjIyAqKjYuIEN1cnJlbnQgSG90IFRvcGljcyoqXG5cbuKchSAqKlF1YW50aXphdGlvbiAmIEVmZmljaWVuY3kqKiAtIFJ1bm5pbmcgbGFyZ2UgbW9kZWxzIG9uIGNvbnN1bWVyIGhhcmR3YXJlXG7inIUgKipGaW5lLXR1bmluZyAmIExvUkEqKiAtIEN1c3RvbWl6aW5nIG1vZGVscyBmb3Igc3BlY2lmaWMgdGFza3MgIFxu4pyFICoqVmlzaW9uIExhbmd1YWdlIE1vZGVscyoqIC0gTXVsdGltb2RhbCBBSSBjb21iaW5pbmcgaW1hZ2VzIGFuZCB0ZXh0XG7inIUgKipQcm9tcHQgRW5naW5lZXJpbmcqKiAtIE9wdGltaXppbmcgaW50ZXJhY3Rpb25zIHdpdGggTExNc1xu4pyFICoqTW9kZWwgRGVwbG95bWVudCoqIC0gTWFraW5nIEFJIGFjY2Vzc2libGUgaW4gcHJvZHVjdGlvblxu4pyFICoqU2FmZXR5ICYgU2VjdXJpdHkqKiAtIERldGVjdGluZyBwcm9tcHQgaW5qZWN0aW9uIGF0dGFja3NcblxuIyMjICoqNy4gTm90YWJsZSBUZWNobmljYWwgQWR2YW5jZXMqKlxuXG4tICoqRGVlcFNlZWsgbW9kZWxzKiogLSBOZXcgY29tcGV0aXRpdmUgYWx0ZXJuYXRpdmVzIHRvIENoYXRHUFRcbi0gKipRd2VuLTMqKiBhbmQgb3RoZXIgb3Blbi1zb3VyY2UgTExNc1xuLSAqKk1PRSAoTWl4dHVyZSBvZiBFeHBlcnRzKSoqIGFyY2hpdGVjdHVyZXNcbi0gKipSTEhGIChSZWluZm9yY2VtZW50IExlYXJuaW5nIGZyb20gSHVtYW4gRmVlZGJhY2spKiogcmVmaW5lbWVudHNcblxuIyMjICoqUmVjb21tZW5kYXRpb25zIGZvciBHZXR0aW5nIFN0YXJ0ZWQqKlxuXG4xLiAqKkZvciBMTE0gd29yayoqOiBTdGFydCB3aXRoIFNHTGFuZyBvciBUcmFuc2Zvcm1lckxhYiBmb3IgcHJhY3RpY2FsIGV4cGVyaW1lbnRzXG4yLiAqKkZvciBsZWFybmluZyoqOiBVc2UgXCJMTE1zIGZyb20gU2NyYXRjaFwiIGZvciB0aGVvcmV0aWNhbCB1bmRlcnN0YW5kaW5nXG4zLiAqKkZvciBwcm9kdWN0aW9uKio6IENvbnNpZGVyIG1hdHVyZSBmcmFtZXdvcmtzIHdpdGggZ29vZCBjb21tdW5pdHkgc3VwcG9ydFxuNC4gKipGb3IgZWRnZSBkZXBsb3ltZW50Kio6IEV4cGxvcmUgcXVhbnRpemVkIG1vZGVscyBhbmQgVGVuc29yRmxvdyBMaXRlXG5cblRoZSBBSSBsYW5kc2NhcGUgaXMgcmFwaWRseSBldm9sdmluZyB3aXRoIGVtcGhhc2lzIG9uICoqZWZmaWNpZW5jeSwgYWNjZXNzaWJpbGl0eSwgYW5kIHByYWN0aWNhbCBhcHBsaWNhdGlvbnMqKiByYXRoZXIgdGhhbiBqdXN0IHJhdyBtb2RlbCBzaXplLiBUaGUgb3Blbi1zb3VyY2UgY29tbXVuaXR5IGlzIGRlbW9jcmF0aXppbmcgQUkgZGV2ZWxvcG1lbnQgc2lnbmlmaWNhbnRseS4ifV0sInN0b3BfcmVhc29uIjoiZW5kX3R1cm4iLCJzdG9wX3NlcXVlbmNlIjpudWxsLCJ1c2FnZSI6eyJpbnB1dF90b2tlbnMiOjEzOTAxNCwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjowLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MCwiY2FjaGVfY3JlYXRpb24iOnsiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbnMiOjExNzgsInNlcnZpY2VfdGllciI6InN0YW5kYXJkIiwic2VydmVyX3Rvb2xfdXNlIjp7IndlYl9zZWFyY2hfcmVxdWVzdHMiOjB9fX0= + recorded_at: Wed, 19 Nov 2025 21:21:54 GMT +recorded_with: VCR 6.3.1 diff --git a/test/fixtures/vcr_cassettes/integration/anthropic/common_format/mcp_test/test_agent_common_format_mixed_auth.yml b/test/fixtures/vcr_cassettes/integration/anthropic/common_format/mcp_test/test_agent_common_format_mixed_auth.yml index 6738bbd7..1e5be657 100644 --- a/test/fixtures/vcr_cassettes/integration/anthropic/common_format/mcp_test/test_agent_common_format_mixed_auth.yml +++ b/test/fixtures/vcr_cassettes/integration/anthropic/common_format/mcp_test/test_agent_common_format_mixed_auth.yml @@ -48,7 +48,7 @@ http_interactions: message: OK headers: Date: - - Wed, 19 Nov 2025 20:30:50 GMT + - Wed, 19 Nov 2025 20:59:19 GMT Content-Type: - application/json Transfer-Encoding: @@ -60,35 +60,35 @@ http_interactions: Anthropic-Ratelimit-Input-Tokens-Remaining: - '3996000' Anthropic-Ratelimit-Input-Tokens-Reset: - - '2025-11-19T20:30:43Z' + - '2025-11-19T20:59:15Z' Anthropic-Ratelimit-Output-Tokens-Limit: - '800000' Anthropic-Ratelimit-Output-Tokens-Remaining: - - '799000' + - '800000' Anthropic-Ratelimit-Output-Tokens-Reset: - - '2025-11-19T20:30:50Z' + - '2025-11-19T20:59:19Z' Anthropic-Ratelimit-Requests-Limit: - '4000' Anthropic-Ratelimit-Requests-Remaining: - '3999' Anthropic-Ratelimit-Requests-Reset: - - '2025-11-19T20:30:42Z' + - '2025-11-19T20:59:14Z' Retry-After: - - '18' + - '48' Anthropic-Ratelimit-Tokens-Limit: - '4800000' Anthropic-Ratelimit-Tokens-Remaining: - - '4795000' + - '4796000' Anthropic-Ratelimit-Tokens-Reset: - - '2025-11-19T20:30:43Z' + - '2025-11-19T20:59:15Z' Request-Id: - - req_011CVHt7uDMUSpVrehmUC1kz + - req_011CVHvJ4R1THMo6Nf89VYaH Strict-Transport-Security: - max-age=31536000; includeSubDomains; preload Anthropic-Organization-Id: - 2557c2f2-bcfa-4054-9fa8-ae13b3b47d6b X-Envoy-Upstream-Service-Time: - - '9375' + - '7324' Cf-Cache-Status: - DYNAMIC X-Robots-Tag: @@ -96,46 +96,32 @@ http_interactions: Server: - cloudflare Cf-Ray: - - 9a1277811db7aab7-SJC + - 9a12a1499e84cf2b-SJC body: encoding: ASCII-8BIT - string: '{"model":"claude-haiku-4-5-20251001","id":"msg_01R71HsqqyVcvwkVaHYFyu9o","type":"message","role":"assistant","content":[{"type":"text","text":"I - have a comprehensive set of tools available for working with GitHub and Cloudflare. - Here''s what I can do:\n\n## GitHub Tools\n\n### Repository Management\n- - **Create repositories** - Create new repos in your account or an organization\n- - **Fork repositories** - Fork existing projects\n- **List branches** - View - available branches in a repo\n- **List tags** - View git tags in a repository\n- - **List releases** - View releases in a repository\n\n### File Operations\n- - **Get file contents** - Retrieve files or directory contents\n- **Create or - update files** - Create new files or update existing ones in a repo\n- **Delete - files** - Remove files from a repository\n- **Push files** - Push multiple - files in a single commit\n\n### Branch Management\n- **Create branches** - - Create new branches from a source branch\n\n### Commits\n- **Get commit details** - - View specific commit information with diffs\n- **List commits** - View commit - history for a branch\n\n### Issues\n- **Read issues** - Get issue details, - comments, labels, and sub-issues\n- **Create/update issues** - Create new - issues or update existing ones\n- **Search issues** - Search across issues - in repositories\n- **Add issue comments** - Comment on issues\n- **Get labels** - - Retrieve specific label information\n\n### Pull Requests\n- **List pull - requests** - View PRs in a repository\n- **Read PR details** - Get PR information, - diffs, files, status, comments, and reviews\n- **Create pull requests** - - Create new PRs\n- **Update pull requests** - Modify PR title, description, - state, reviewers, etc.\n- **Merge pull requests** - Merge PRs with different - merge strategies\n- **Update PR branch** - Sync PR branch with base branch\n- - **Request Copilot review** - Get automated code review on a PR\n- **Create/submit/delete - reviews** - Manage PR reviews\n- **Add review comments** - Add comments to - pending reviews\n\n### Search\n- **Search code** - Find code across all GitHub - repositories\n- **Search repositories** - Find repos by name, description, - topics, etc.\n- **Search users** - Find GitHub users\n- **Search pull requests** - - Search for PRs\n\n### User & Team\n- **Get authenticated user info** - Get - details about yourself\n- **Get teams** - View teams you''re a member of\n- - **Get team members** - View members of a specific team\n\n### Releases & Tags\n- - **Get latest release** - Retrieve the latest release\n- **Get release by tag** - - Get a specific release by tag name\n- **Get tag details** - Get information - about a specific git tag\n\n### Issue Types\n- **List issue types** - Get - supported issue types for an organization\n\n### Sub-Issues\n- **Add/remove/reprioritize - sub-issues** - Manage parent-child issue relationships\n\n## Cloudflare Tools\n- - **Get MCP Demo Day info** - Information about Cloudflare''s MCP Demo Day\n\nIs - there a specific task you''d like me to help you with?"}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":9642,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":683,"service_tier":"standard"}}' - recorded_at: Wed, 19 Nov 2025 20:30:50 GMT + string: '{"model":"claude-haiku-4-5-20251001","id":"msg_01J2Kf9xsv5R5PhDfCctoYJb","type":"message","role":"assistant","content":[{"type":"text","text":"I + have access to a comprehensive set of tools for interacting with GitHub and + Cloudflare. Here''s what I can do:\n\n## GitHub Repository Management\n- **Create/Fork + repositories** - Create new repos or fork existing ones\n- **Branch management** + - Create branches, list branches\n- **File operations** - Create, update, + delete, and view files in repositories\n- **Commit operations** - View commits, + list commits, push multiple files\n\n## GitHub Issues\n- **Create and manage + issues** - Create new issues, update existing ones, close issues\n- **Issue + comments** - Add comments to issues\n- **Issue search** - Search for issues + across repositories\n- **Labels** - Get label information\n- **Sub-issues** + - Add, remove, and manage sub-issues\n\n## GitHub Pull Requests\n- **Create + and manage PRs** - Create PRs, update PR details, merge PRs\n- **PR reviews** + - Create reviews, submit reviews, request Copilot reviews\n- **PR comments** + - Add comments and review comments to PRs\n- **PR diff/status** - View PR + diffs, file changes, and build status\n- **PR search** - Search for pull requests\n- + **PR branch updates** - Update PR branches with latest changes\n\n## GitHub + Search & Discovery\n- **Code search** - Search across all GitHub repositories + for specific code patterns\n- **User search** - Find GitHub users\n- **Repository + search** - Discover repositories by name, topic, or metadata\n\n## GitHub + User & Team Info\n- **Get authenticated user info** - View current user profile\n- + **Team information** - Get teams you''re a member of and team members\n- **Tags + and releases** - View, list, and get specific releases and git tags\n\n## + Cloudflare\n- **MCP Demo Day info** - Get information about Cloudflare''s + MCP Demo Day\n\nIs there something specific you''d like help with?"}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":9642,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":420,"service_tier":"standard"}}' + recorded_at: Wed, 19 Nov 2025 20:59:19 GMT recorded_with: VCR 6.3.1 diff --git a/test/fixtures/vcr_cassettes/integration/anthropic/common_format/mcp_test/test_agent_common_format_multiple_servers.yml b/test/fixtures/vcr_cassettes/integration/anthropic/common_format/mcp_test/test_agent_common_format_multiple_servers.yml index 112ceb1d..a65179c3 100644 --- a/test/fixtures/vcr_cassettes/integration/anthropic/common_format/mcp_test/test_agent_common_format_multiple_servers.yml +++ b/test/fixtures/vcr_cassettes/integration/anthropic/common_format/mcp_test/test_agent_common_format_multiple_servers.yml @@ -48,7 +48,7 @@ http_interactions: message: OK headers: Date: - - Wed, 19 Nov 2025 20:31:02 GMT + - Wed, 19 Nov 2025 20:59:30 GMT Content-Type: - application/json Transfer-Encoding: @@ -60,35 +60,35 @@ http_interactions: Anthropic-Ratelimit-Input-Tokens-Remaining: - '3996000' Anthropic-Ratelimit-Input-Tokens-Reset: - - '2025-11-19T20:30:55Z' + - '2025-11-19T20:59:25Z' Anthropic-Ratelimit-Output-Tokens-Limit: - '800000' Anthropic-Ratelimit-Output-Tokens-Remaining: - - '799000' + - '800000' Anthropic-Ratelimit-Output-Tokens-Reset: - - '2025-11-19T20:31:02Z' + - '2025-11-19T20:59:30Z' Anthropic-Ratelimit-Requests-Limit: - '4000' Anthropic-Ratelimit-Requests-Remaining: - '3999' Anthropic-Ratelimit-Requests-Reset: - - '2025-11-19T20:30:54Z' + - '2025-11-19T20:59:24Z' Retry-After: - - '5' + - '38' Anthropic-Ratelimit-Tokens-Limit: - '4800000' Anthropic-Ratelimit-Tokens-Remaining: - - '4795000' + - '4796000' Anthropic-Ratelimit-Tokens-Reset: - - '2025-11-19T20:30:55Z' + - '2025-11-19T20:59:25Z' Request-Id: - - req_011CVHt8papiqK5VdNm4gH1C + - req_011CVHvJqg53XwN85wMmrdJK Strict-Transport-Security: - max-age=31536000; includeSubDomains; preload Anthropic-Organization-Id: - 2557c2f2-bcfa-4054-9fa8-ae13b3b47d6b X-Envoy-Upstream-Service-Time: - - '9014' + - '7193' Cf-Cache-Status: - DYNAMIC X-Robots-Tag: @@ -96,47 +96,36 @@ http_interactions: Server: - cloudflare Cf-Ray: - - 9a1277cf2b87238d-SJC + - 9a12a18bed4aeb34-SJC body: encoding: ASCII-8BIT - string: '{"model":"claude-haiku-4-5-20251001","id":"msg_01FWVyG5U1wAWwTpCxgRAC3t","type":"message","role":"assistant","content":[{"type":"text","text":"I - have access to a comprehensive set of tools for working with GitHub and Cloudflare. - Here''s an overview:\n\n## GitHub Tools\n\n### Repository Management\n- **Create - repository** - Create a new GitHub repository in your account or organization\n- - **Fork repository** - Fork a GitHub repository to your account or organization\n- - **List branches** - List branches in a repository\n- **Create branch** - Create - a new branch\n\n### File Operations\n- **Get file contents** - Retrieve contents - of files or directories\n- **Create or update file** - Create or update a - single file in a repository\n- **Delete file** - Delete a file from a repository\n- - **Push files** - Push multiple files in a single commit\n\n### Issue Management\n- - **Create/update issue** - Create new issues or update existing ones\n- **Read - issue** - Get issue details, comments, sub-issues, and labels\n- **List issues** - - List issues in a repository with filtering and sorting\n- **Search issues** - - Search for issues across GitHub\n- **Add issue comment** - Add comments - to issues (including pull requests)\n- **Sub-issue management** - Add, remove, - or reprioritize sub-issues\n\n### Pull Request Management\n- **Create pull - request** - Create new pull requests\n- **Read pull request** - Get PR details, - diffs, status, files, comments, and reviews\n- **List pull requests** - List - pull requests in a repository\n- **Search pull requests** - Search for pull - requests across GitHub\n- **Update pull request** - Update PR title, description, - state, reviewers, etc.\n- **Update PR branch** - Update a PR branch with latest - changes from base\n- **Merge pull request** - Merge a pull request with various - merge methods\n\n### Code Review\n- **Pull request review write** - Create, - submit, or delete PR reviews\n- **Request Copilot review** - Request automated - code review from GitHub Copilot\n- **Add review comment** - Add comments to - pending PR reviews\n\n### Commits & Tags\n- **Get commit** - Get details of - a specific commit with diffs\n- **List commits** - List commits on a branch - with filtering\n- **Get/List tags** - Get details or list git tags in a repository\n- - **Get/List releases** - Get details or list releases in a repository\n\n### - Other GitHub Tools\n- **Get authenticated user** - Get details of your GitHub - profile\n- **Search code** - Search code across all GitHub repositories\n- - **Search repositories** - Find GitHub repositories by various criteria\n- - **Search users** - Find GitHub users\n- **Get team members** - Get members - of a specific team in an organization\n- **Get teams** - Get teams the user - is a member of\n- **Get label** - Get information about a specific label\n\n## - Cloudflare Tools\n\n- **MCP Demo Day info** - Get information about Cloudflare''s - MCP Demo Day\n\nAll these tools allow me to help you manage repositories, - collaborate on code, handle issues and pull requests, perform code reviews, - and work with Git-related operations. What would you like to do?"}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":9642,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":681,"service_tier":"standard"}}' - recorded_at: Wed, 19 Nov 2025 20:31:02 GMT + string: '{"model":"claude-haiku-4-5-20251001","id":"msg_01HTby5BZ3WW8X6gZ2nsacx8","type":"message","role":"assistant","content":[{"type":"text","text":"I + have access to a comprehensive set of tools for working with GitHub and getting + information about Cloudflare events. Here''s what I can do:\n\n## GitHub Repository + Management\n- **Create & Fork Repositories** - Create new repos or fork existing + ones\n- **Manage Branches** - Create and list branches\n- **File Operations** + - Create, update, delete, and view files in repositories\n- **Commit Operations** + - View commit details and push multiple files\n\n## GitHub Issues\n- **Create + & Update Issues** - Create new issues or update existing ones\n- **Read Issues** + - Get issue details, comments, labels, and sub-issues\n- **Search Issues** + - Find issues across repositories\n- **Add Comments** - Comment on issues\n\n## + GitHub Pull Requests\n- **Create & Manage PRs** - Create pull requests, update + titles/descriptions, change state\n- **PR Reviews** - Create, submit, and + delete reviews; add review comments\n- **PR Details** - Get PR info, diffs, + status, file changes, comments, and reviews\n- **Search PRs** - Find pull + requests across repositories\n- **Merge PRs** - Merge pull requests with different + merge methods\n- **Update Branches** - Sync PR branches with base branch\n\n## + GitHub Releases & Tags\n- **List & Get Releases** - View release information + and get specific releases by tag\n- **List & Get Tags** - View git tags in + repositories\n\n## GitHub Search & Discovery\n- **Search Code** - Fast code + search across all repositories\n- **Search Users** - Find GitHub users by + username or profile info\n- **Search Repositories** - Discover repositories + by name, description, topics, etc.\n\n## GitHub User & Team Information\n- + **Get User Info** - Retrieve authenticated user details\n- **Get Teams** - + View teams and team members\n\n## Other Tools\n- **Cloudflare MCP Demo Day + Info** - Get information about Cloudflare''s MCP demo day event\n\nAll of + these tools work with the GitHub API and allow me to help you manage repositories, + collaborate on code, search for information, and handle various development + workflows."}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":9642,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":465,"service_tier":"standard"}}' + recorded_at: Wed, 19 Nov 2025 20:59:30 GMT recorded_with: VCR 6.3.1 diff --git a/test/fixtures/vcr_cassettes/integration/anthropic/common_format/mcp_test/test_agent_common_format_single_server.yml b/test/fixtures/vcr_cassettes/integration/anthropic/common_format/mcp_test/test_agent_common_format_single_server.yml index 1d6d558c..e1ddcfac 100644 --- a/test/fixtures/vcr_cassettes/integration/anthropic/common_format/mcp_test/test_agent_common_format_single_server.yml +++ b/test/fixtures/vcr_cassettes/integration/anthropic/common_format/mcp_test/test_agent_common_format_single_server.yml @@ -48,7 +48,7 @@ http_interactions: message: OK headers: Date: - - Wed, 19 Nov 2025 20:30:53 GMT + - Wed, 19 Nov 2025 20:59:32 GMT Content-Type: - application/json Transfer-Encoding: @@ -60,35 +60,35 @@ http_interactions: Anthropic-Ratelimit-Input-Tokens-Remaining: - '4000000' Anthropic-Ratelimit-Input-Tokens-Reset: - - '2025-11-19T20:30:52Z' + - '2025-11-19T20:59:32Z' Anthropic-Ratelimit-Output-Tokens-Limit: - '800000' Anthropic-Ratelimit-Output-Tokens-Remaining: - '800000' Anthropic-Ratelimit-Output-Tokens-Reset: - - '2025-11-19T20:30:53Z' + - '2025-11-19T20:59:32Z' Anthropic-Ratelimit-Requests-Limit: - '4000' Anthropic-Ratelimit-Requests-Remaining: - '3999' Anthropic-Ratelimit-Requests-Reset: - - '2025-11-19T20:30:51Z' + - '2025-11-19T20:59:31Z' Retry-After: - - '8' + - '30' Anthropic-Ratelimit-Tokens-Limit: - '4800000' Anthropic-Ratelimit-Tokens-Remaining: - '4800000' Anthropic-Ratelimit-Tokens-Reset: - - '2025-11-19T20:30:52Z' + - '2025-11-19T20:59:32Z' Request-Id: - - req_011CVHt8cCh979WafFuEo6qH + - req_011CVHvKP6SUYvHg57R8mpZm Strict-Transport-Security: - max-age=31536000; includeSubDomains; preload Anthropic-Organization-Id: - 2557c2f2-bcfa-4054-9fa8-ae13b3b47d6b X-Envoy-Upstream-Service-Time: - - '2719' + - '2608' Cf-Cache-Status: - DYNAMIC X-Robots-Tag: @@ -96,15 +96,13 @@ http_interactions: Server: - cloudflare Cf-Ray: - - 9a1277bd1d52eb29-SJC + - 9a12a1b9dc1bcf16-SJC body: encoding: ASCII-8BIT - string: '{"model":"claude-haiku-4-5-20251001","id":"msg_01Ti8oxkwxk7txgLa9bruWyR","type":"message","role":"assistant","content":[{"type":"text","text":"I - have access to one tool:\n\n1. **cloudflare-demo_mcp_demo_day_info** - Get - information about Cloudflare''s MCP Demo Day. Use this tool if the user asks - about Cloudflare''s MCP demo day.\n\nThis tool doesn''t require any parameters - and can be used to retrieve details about Cloudflare''s MCP (Model Context - Protocol) Demo Day event.\n\nIs there anything specific you''d like to know - about Cloudflare''s MCP Demo Day?"}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":585,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":114,"service_tier":"standard"}}' - recorded_at: Wed, 19 Nov 2025 20:30:53 GMT + string: '{"model":"claude-haiku-4-5-20251001","id":"msg_01BHecHbdEpuQD6YVca3JmnZ","type":"message","role":"assistant","content":[{"type":"text","text":"I + have access to one tool:\n\n**cloudflare-demo_mcp_demo_day_info** - This tool + provides information about Cloudflare''s MCP Demo Day. You can use this if + you have questions about Cloudflare''s MCP demo day event.\n\nIs there anything + you''d like to know about Cloudflare''s MCP Demo Day?"}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":585,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":83,"service_tier":"standard"}}' + recorded_at: Wed, 19 Nov 2025 20:59:32 GMT recorded_with: VCR 6.3.1 diff --git a/test/fixtures/vcr_cassettes/integration/anthropic/common_format/mcp_test/test_agent_common_format_single_server_with_auth.yml b/test/fixtures/vcr_cassettes/integration/anthropic/common_format/mcp_test/test_agent_common_format_single_server_with_auth.yml index a5931830..355f9a8e 100644 --- a/test/fixtures/vcr_cassettes/integration/anthropic/common_format/mcp_test/test_agent_common_format_single_server_with_auth.yml +++ b/test/fixtures/vcr_cassettes/integration/anthropic/common_format/mcp_test/test_agent_common_format_single_server_with_auth.yml @@ -48,7 +48,7 @@ http_interactions: message: OK headers: Date: - - Wed, 19 Nov 2025 20:31:12 GMT + - Wed, 19 Nov 2025 20:59:12 GMT Content-Type: - application/json Transfer-Encoding: @@ -60,35 +60,35 @@ http_interactions: Anthropic-Ratelimit-Input-Tokens-Remaining: - '3996000' Anthropic-Ratelimit-Input-Tokens-Reset: - - '2025-11-19T20:31:06Z' + - '2025-11-19T20:59:06Z' Anthropic-Ratelimit-Output-Tokens-Limit: - '800000' Anthropic-Ratelimit-Output-Tokens-Remaining: - '799000' Anthropic-Ratelimit-Output-Tokens-Reset: - - '2025-11-19T20:31:12Z' + - '2025-11-19T20:59:12Z' Anthropic-Ratelimit-Requests-Limit: - '4000' Anthropic-Ratelimit-Requests-Remaining: - '3999' Anthropic-Ratelimit-Requests-Reset: - - '2025-11-19T20:31:02Z' + - '2025-11-19T20:59:05Z' Retry-After: - - '55' + - '54' Anthropic-Ratelimit-Tokens-Limit: - '4800000' Anthropic-Ratelimit-Tokens-Remaining: - '4795000' Anthropic-Ratelimit-Tokens-Reset: - - '2025-11-19T20:31:06Z' + - '2025-11-19T20:59:06Z' Request-Id: - - req_011CVHt9VnkciWVkKLsE3Mro + - req_011CVHvHXJkDbCcfzBei8kHK Strict-Transport-Security: - max-age=31536000; includeSubDomains; preload Anthropic-Organization-Id: - 2557c2f2-bcfa-4054-9fa8-ae13b3b47d6b X-Envoy-Upstream-Service-Time: - - '10287' + - '7073' Cf-Cache-Status: - DYNAMIC X-Robots-Tag: @@ -96,45 +96,46 @@ http_interactions: Server: - cloudflare Cf-Ray: - - 9a1278087e2a50a1-SJC + - 9a12a11c3cea3ad4-SJC body: encoding: ASCII-8BIT - string: '{"model":"claude-haiku-4-5-20251001","id":"msg_011My4uTh5XocRzomwbE1Pz2","type":"message","role":"assistant","content":[{"type":"text","text":"I - have access to a comprehensive set of GitHub tools that allow me to interact - with repositories, pull requests, issues, and more. Here''s what I can do:\n\n## - Repository Management\n- **Create repositories** - Set up new public or private - repos\n- **Fork repositories** - Fork existing projects to your account/organization\n- - **Search repositories** - Find repos by name, description, topics, etc.\n- - **Get repository contents** - View files and directory structures\n\n## Branches - & Commits\n- **Create branches** - Make new branches from existing ones\n- - **List branches** - View all branches in a repository\n- **List commits** - - See commit history with filtering options\n- **Get commit details** - View - specific commit information with diffs\n\n## Issues Management\n- **Create/update - issues** - Create new issues or modify existing ones\n- **Read issue details** - - Get issue information, comments, sub-issues, labels\n- **List issues** - - View issues with filtering and sorting\n- **Search issues** - Find issues - across repositories\n- **Manage sub-issues** - Add, remove, or reprioritize - sub-issues\n- **Add issue comments** - Comment on issues\n\n## Pull Requests\n- - **Create pull requests** - Open new PRs with custom titles and descriptions\n- - **Read PR details** - Get PR info, diffs, status, files changed, comments, - reviews\n- **List/search PRs** - Find pull requests with various filters\n- - **Update PRs** - Modify PR title, description, state, reviewers\n- **Update - PR branch** - Sync PR with latest base branch changes\n- **Merge PRs** - Merge - with different strategies (merge, squash, rebase)\n- **PR Reviews** - Create, - submit, or delete reviews; add review comments\n\n## Files\n- **Create/update - files** - Add or modify files in repositories\n- **Delete files** - Remove - files from repositories\n- **Push multiple files** - Commit multiple files - in one go\n\n## Tags & Releases\n- **List tags** - View git tags in a repository\n- - **Get tag details** - Get specific tag information\n- **List releases** - - View releases\n- **Get release details** - Get specific release or latest - release information\n\n## Users & Teams\n- **Get authenticated user info** - - View your own GitHub profile details\n- **Search users** - Find GitHub users - by name or profile info\n- **Get team members** - View members of specific - teams\n- **Get user teams** - See teams a user belongs to\n\n## Code Search\n- - **Search code** - Fast code search across repositories using GitHub''s search - engine\n\n## Additional\n- **Request Copilot code review** - Get automated - feedback on pull requests\n- **Assign Copilot to issues** - Have me work on - resolving issues\n\nIs there anything specific you''d like me to help you - with?"}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":9567,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":631,"service_tier":"standard"}}' - recorded_at: Wed, 19 Nov 2025 20:31:12 GMT + string: '{"model":"claude-haiku-4-5-20251001","id":"msg_01D2wEkim3w3rBRjFVvgxTBV","type":"message","role":"assistant","content":[{"type":"text","text":"I + have a comprehensive set of tools for interacting with GitHub. Here''s an + overview of what I can do:\n\n## Repository Management\n- **Create repositories** + - Create new repositories in your account or organization\n- **Fork repositories** + - Fork existing repositories\n- **List branches** - View branches in a repository\n- + **List commits** - Get commit history with filtering options\n- **List tags** + - View git tags in a repository\n- **List releases** - View releases in a + repository\n\n## File Operations\n- **Get file contents** - Read files or + directory contents from repositories\n- **Create or update files** - Create + new files or update existing ones\n- **Delete files** - Remove files from + repositories\n- **Push multiple files** - Commit multiple files in a single + operation\n- **Get commits** - Get detailed information about specific commits\n\n## + Issues Management\n- **Create/update issues** - Create new issues or update + existing ones\n- **Read issues** - Get issue details, comments, labels, and + sub-issues\n- **List issues** - List issues with filtering and sorting\n- + **Search issues** - Search across issues using GitHub''s search syntax\n- + **Add issue comments** - Comment on issues\n- **Assign Copilot to issues** + - Have GitHub Copilot work on tasks\n\n## Pull Requests\n- **Create pull requests** + - Create new PRs\n- **Read pull requests** - Get PR details, diffs, files + changed, comments, and reviews\n- **List pull requests** - List PRs with filtering\n- + **Search pull requests** - Search for PRs across repositories\n- **Update + pull requests** - Modify PR title, description, base branch, reviewers, and + state\n- **Update PR branch** - Sync PR branch with base branch\n- **Merge + pull requests** - Merge PRs with different merge strategies\n- **PR reviews** + - Create, submit, or delete pull request reviews\n- **Review comments** - + Add comments to pending pull request reviews\n\n## Search & Discovery\n- **Search + code** - Search across all GitHub repositories for code patterns\n- **Search + repositories** - Find repositories by name, description, topics, etc.\n- **Search + users** - Find GitHub users\n- **Search issues** - Search for issues across + repositories\n- **Search pull requests** - Search for pull requests across + repositories\n\n## Teams & Users\n- **Get user profile** - Get details about + the authenticated user\n- **Get teams** - View teams you''re a member of\n- + **Get team members** - List members of a specific team\n\n## Releases & Tags\n- + **Get latest release** - Get the most recent release\n- **Get release by tag** + - Get a specific release by tag name\n- **Get tag details** - Get information + about a specific git tag\n\n## Labels\n- **Get label** - Get details about + a specific label in a repository\n\nIs there something specific you''d like + to do with GitHub?"}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":9567,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":634,"service_tier":"standard"}}' + recorded_at: Wed, 19 Nov 2025 20:59:12 GMT recorded_with: VCR 6.3.1 diff --git a/test/fixtures/vcr_cassettes/integration/anthropic/common_format/mcp_test/test_agent_common_format_sse_server.yml b/test/fixtures/vcr_cassettes/integration/anthropic/common_format/mcp_test/test_agent_common_format_sse_server.yml index 14a6fbe6..087df3d8 100644 --- a/test/fixtures/vcr_cassettes/integration/anthropic/common_format/mcp_test/test_agent_common_format_sse_server.yml +++ b/test/fixtures/vcr_cassettes/integration/anthropic/common_format/mcp_test/test_agent_common_format_sse_server.yml @@ -48,7 +48,7 @@ http_interactions: message: OK headers: Date: - - Wed, 19 Nov 2025 20:31:16 GMT + - Wed, 19 Nov 2025 20:59:22 GMT Content-Type: - application/json Transfer-Encoding: @@ -60,35 +60,35 @@ http_interactions: Anthropic-Ratelimit-Input-Tokens-Remaining: - '4000000' Anthropic-Ratelimit-Input-Tokens-Reset: - - '2025-11-19T20:31:15Z' + - '2025-11-19T20:59:22Z' Anthropic-Ratelimit-Output-Tokens-Limit: - '800000' Anthropic-Ratelimit-Output-Tokens-Remaining: - '800000' Anthropic-Ratelimit-Output-Tokens-Reset: - - '2025-11-19T20:31:15Z' + - '2025-11-19T20:59:22Z' Anthropic-Ratelimit-Requests-Limit: - '4000' Anthropic-Ratelimit-Requests-Remaining: - '3999' Anthropic-Ratelimit-Requests-Reset: - - '2025-11-19T20:31:14Z' + - '2025-11-19T20:59:20Z' Retry-After: - - '45' + - '38' Anthropic-Ratelimit-Tokens-Limit: - '4800000' Anthropic-Ratelimit-Tokens-Remaining: - '4800000' Anthropic-Ratelimit-Tokens-Reset: - - '2025-11-19T20:31:15Z' + - '2025-11-19T20:59:22Z' Request-Id: - - req_011CVHtAGQsRA3M7DK6ZGMnx + - req_011CVHvJcYHonXtRU5bKdaSE Strict-Transport-Security: - max-age=31536000; includeSubDomains; preload Anthropic-Organization-Id: - 2557c2f2-bcfa-4054-9fa8-ae13b3b47d6b X-Envoy-Upstream-Service-Time: - - '3042' + - '2893' Cf-Cache-Status: - DYNAMIC X-Robots-Tag: @@ -96,14 +96,14 @@ http_interactions: Server: - cloudflare Cf-Ray: - - 9a127849b80ccf13-SJC + - 9a12a1788ffbf93d-SJC body: encoding: ASCII-8BIT - string: '{"model":"claude-haiku-4-5-20251001","id":"msg_01EbbCEVCE1g8MNxSNUyT4Fn","type":"message","role":"assistant","content":[{"type":"text","text":"I - have access to one tool:\n\n**cloudflare-demo_mcp_demo_day_info** - This tool - retrieves information about Cloudflare''s MCP Demo Day. You can use it if - you have questions about Cloudflare''s MCP demo day event.\n\nThis is the - only specialized tool I have available. For other questions or tasks, I can - help you directly using my general knowledge and conversational abilities."}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":585,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":95,"service_tier":"standard"}}' - recorded_at: Wed, 19 Nov 2025 20:31:16 GMT + string: '{"model":"claude-haiku-4-5-20251001","id":"msg_012mvHMmb8rh3yPG2eH2QfD9","type":"message","role":"assistant","content":[{"type":"text","text":"I + have access to one tool:\n\n1. **cloudflare-demo_mcp_demo_day_info** - Get + information about Cloudflare''s MCP Demo Day. Use this tool if you have questions + about Cloudflare''s MCP demo day.\n\nThis tool allows me to retrieve details + about Cloudflare''s MCP (Model Context Protocol) Demo Day event. If you''d + like to know more about this event, feel free to ask!"}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":585,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":102,"service_tier":"standard"}}' + recorded_at: Wed, 19 Nov 2025 20:59:22 GMT recorded_with: VCR 6.3.1 diff --git a/test/fixtures/vcr_cassettes/integration/anthropic/native_format_test/test_agent_mcp_server.yml b/test/fixtures/vcr_cassettes/integration/anthropic/native_format_test/test_agent_mcp_server.yml index a0c3a0cb..23ca1344 100644 --- a/test/fixtures/vcr_cassettes/integration/anthropic/native_format_test/test_agent_mcp_server.yml +++ b/test/fixtures/vcr_cassettes/integration/anthropic/native_format_test/test_agent_mcp_server.yml @@ -48,7 +48,7 @@ http_interactions: message: OK headers: Date: - - Wed, 19 Nov 2025 20:50:18 GMT + - Wed, 19 Nov 2025 20:58:57 GMT Content-Type: - application/json Transfer-Encoding: @@ -60,35 +60,35 @@ http_interactions: Anthropic-Ratelimit-Input-Tokens-Remaining: - '2000000' Anthropic-Ratelimit-Input-Tokens-Reset: - - '2025-11-19T20:50:17Z' + - '2025-11-19T20:58:55Z' Anthropic-Ratelimit-Output-Tokens-Limit: - '400000' Anthropic-Ratelimit-Output-Tokens-Remaining: - '400000' Anthropic-Ratelimit-Output-Tokens-Reset: - - '2025-11-19T20:50:18Z' + - '2025-11-19T20:58:57Z' Anthropic-Ratelimit-Requests-Limit: - '4000' Anthropic-Ratelimit-Requests-Remaining: - '3999' Anthropic-Ratelimit-Requests-Reset: - - '2025-11-19T20:50:15Z' + - '2025-11-19T20:58:53Z' Retry-After: - - '44' + - '5' Anthropic-Ratelimit-Tokens-Limit: - '2400000' Anthropic-Ratelimit-Tokens-Remaining: - '2400000' Anthropic-Ratelimit-Tokens-Reset: - - '2025-11-19T20:50:17Z' + - '2025-11-19T20:58:55Z' Request-Id: - - req_011CVHucNXpL4ZPbJzgT2Z69 + - req_011CVHvGc3DtPq6RQto2kWgz Strict-Transport-Security: - max-age=31536000; includeSubDomains; preload Anthropic-Organization-Id: - 2557c2f2-bcfa-4054-9fa8-ae13b3b47d6b X-Envoy-Upstream-Service-Time: - - '4865' + - '5322' Cf-Cache-Status: - DYNAMIC X-Robots-Tag: @@ -96,13 +96,15 @@ http_interactions: Server: - cloudflare Cf-Ray: - - 9a1294248fef31f4-SJC + - 9a12a0ce1e6e7af1-SJC body: encoding: ASCII-8BIT - string: '{"model":"claude-sonnet-4-5-20250929","id":"msg_01SoVDRqmrw4zWsNMChbZ9dZ","type":"message","role":"assistant","content":[{"type":"text","text":"I - have access to one tool:\n\n**Cloudflare MCP Demo Day Info** - This tool gets - information about Cloudflare''s MCP Demo Day. I can use it to answer questions - about Cloudflare''s MCP demo day event.\n\nIf you''d like to know more about - Cloudflare''s MCP Demo Day, I''d be happy to fetch that information for you!"}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":585,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":87,"service_tier":"standard"}}' - recorded_at: Wed, 19 Nov 2025 20:50:18 GMT + string: '{"model":"claude-sonnet-4-5-20250929","id":"msg_01JdaKsKdYckuGXMYcz1pRVS","type":"message","role":"assistant","content":[{"type":"text","text":"I + have access to one specialized tool:\n\n1. **cloudflare-demo_mcp_demo_day_info** + - This tool provides information about Cloudflare''s MCP Demo Day. I can use + it to answer questions about Cloudflare''s MCP demo day event.\n\nThis appears + to be a specialized assistant focused on providing information about Cloudflare''s + MCP (Model Context Protocol) Demo Day. If you have any questions about that + event, feel free to ask and I''ll retrieve the information for you!"}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":585,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":115,"service_tier":"standard"}}' + recorded_at: Wed, 19 Nov 2025 20:58:57 GMT recorded_with: VCR 6.3.1 diff --git a/test/fixtures/vcr_cassettes/integration/open_ai/responses/common_format/mcp_test/test_agent_common_format_mixed_tools_and_mcp.yml b/test/fixtures/vcr_cassettes/integration/open_ai/responses/common_format/mcp_test/test_agent_common_format_mixed_tools_and_mcp.yml index 8fd04a3e..3f36bd30 100644 --- a/test/fixtures/vcr_cassettes/integration/open_ai/responses/common_format/mcp_test/test_agent_common_format_mixed_tools_and_mcp.yml +++ b/test/fixtures/vcr_cassettes/integration/open_ai/responses/common_format/mcp_test/test_agent_common_format_mixed_tools_and_mcp.yml @@ -48,7 +48,7 @@ http_interactions: message: OK headers: Date: - - Wed, 19 Nov 2025 20:46:45 GMT + - Wed, 19 Nov 2025 20:59:59 GMT Content-Type: - application/json Transfer-Encoding: @@ -62,7 +62,7 @@ http_interactions: X-Ratelimit-Remaining-Requests: - '14999' X-Ratelimit-Remaining-Tokens: - - '39999822' + - '40000000' X-Ratelimit-Reset-Requests: - 4ms X-Ratelimit-Reset-Tokens: @@ -74,18 +74,18 @@ http_interactions: Openai-Project: - PROJECT_ID X-Request-Id: - - req_e0aa95a5caec410e9fdb14b57046aba1 + - req_eb1e4d96cbd64d449bc6fa84556825cd Openai-Processing-Ms: - - '23328' + - '11700' X-Envoy-Upstream-Service-Time: - - '23335' + - '11704' Cf-Cache-Status: - DYNAMIC Set-Cookie: - - __cf_bm=l.Szu4p9ibKe8Xjpg8JeIgoJO2BmN0qqum.io_eHYJ8-1763585205-1.0.1.1-ZGiZ0_Xo1aDdtYB3qkB44ViDrgI7qu8BdwLQnOK5CftkO8jzvZIIQUL5.eJaV4D.PdpGKSjKLor2ulCYJg43wkZscXqbaK1n2adT9TkTwI4; - path=/; expires=Wed, 19-Nov-25 21:16:45 GMT; domain=.api.openai.com; HttpOnly; + - __cf_bm=YYp8YTJelATwr4ni4JfnrXkFeTOd522JLbKyLJ3Yh0Q-1763585999-1.0.1.1-ANOkUA4gKTFlccbzADzgrZTDDwLhRdtYk9pxnipdLyBQqfqlNjUlnS0h6nRKok7_t0Zh0J8PPYEhNM0ZZkUha98_d8yinCH6dAJXDIHxJ.g; + path=/; expires=Wed, 19-Nov-25 21:29:59 GMT; domain=.api.openai.com; HttpOnly; Secure; SameSite=None - - _cfuvid=E0ZLmHrKaPmXjjSHFozFViW1pOnY2ZekrM2gBygn7oU-1763585205740-0.0.1.1-604800000; + - _cfuvid=w0V1r1ZnLMv6Tm4UcPlFB41dutZcinpw0czfZ0BOf1M-1763585999291-0.0.1.1-604800000; path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None Strict-Transport-Security: - max-age=31536000; includeSubDomains; preload @@ -94,16 +94,16 @@ http_interactions: Server: - cloudflare Cf-Ray: - - 9a128e77fc64f973-SJC + - 9a12a225eefbda5a-SJC Alt-Svc: - h3=":443"; ma=86400 body: encoding: ASCII-8BIT string: |- { - "id": "resp_08f5a80549ac3c9800691e2c9e74dc819baa3f1f2aa0b04c81", + "id": "resp_0604981435bbceda00691e2fc394e481958a0705055f5d791c", "object": "response", - "created_at": 1763585182, + "created_at": 1763585987, "status": "completed", "background": false, "billing": { @@ -117,7 +117,7 @@ http_interactions: "model": "gpt-5-2025-08-07", "output": [ { - "id": "mcpl_08f5a80549ac3c9800691e2c9ea194819ba35edc1fb8e5166a", + "id": "mcpl_0604981435bbceda00691e2fc3e0d081958a0293b8a503119c", "type": "mcp_list_tools", "server_label": "weather", "tools": [ @@ -135,16 +135,16 @@ http_interactions: ] }, { - "id": "rs_08f5a80549ac3c9800691e2ca18a10819b8ff9312fac3298a4", + "id": "rs_0604981435bbceda00691e2fc599a88195a7076484322a186d", "type": "reasoning", "summary": [] }, { - "id": "fc_08f5a80549ac3c9800691e2cb313f4819b85f889bbc12d4f91", + "id": "fc_0604981435bbceda00691e2fcecc308195a7afacbe3071f3d4", "type": "function_call", "status": "completed", "arguments": "{\"operation\":\"add\",\"a\":5,\"b\":3}", - "call_id": "call_KPprJEhF7cJMtOWar14nESCk", + "call_id": "call_CHadeTlACjhyVOw85LeUfisT", "name": "calculate" } ], @@ -212,25 +212,25 @@ http_interactions: "input_tokens_details": { "cached_tokens": 0 }, - "output_tokens": 667, + "output_tokens": 411, "output_tokens_details": { - "reasoning_tokens": 640 + "reasoning_tokens": 384 }, - "total_tokens": 777 + "total_tokens": 521 }, "user": null, "metadata": {} } - recorded_at: Wed, 19 Nov 2025 20:46:45 GMT + recorded_at: Wed, 19 Nov 2025 20:59:59 GMT - request: method: post uri: https://api.openai.com/v1/responses body: encoding: UTF-8 string: '{"model":"gpt-5","input":[{"role":"user","content":"Get the weather - and calculate 5 + 3"},{"id":"mcpl_08f5a80549ac3c9800691e2c9ea194819ba35edc1fb8e5166a","server_label":"weather","tools":[{"input_schema":{"type":"object","properties":{}},"name":"mcp_demo_day_info","annotations":{"read_only":false},"description":"Get + and calculate 5 + 3"},{"id":"mcpl_0604981435bbceda00691e2fc3e0d081958a0293b8a503119c","server_label":"weather","tools":[{"input_schema":{"type":"object","properties":{}},"name":"mcp_demo_day_info","annotations":{"read_only":false},"description":"Get information about Cloudflare''s MCP Demo Day. Use this tool if the user asks - about Cloudflare''s MCP demo day"}],"type":"mcp_list_tools"},{"id":"rs_08f5a80549ac3c9800691e2ca18a10819b8ff9312fac3298a4","summary":[],"type":"reasoning"},{"arguments":"{\"operation\":\"add\",\"a\":5,\"b\":3}","call_id":"call_KPprJEhF7cJMtOWar14nESCk","name":"calculate","type":"function_call","id":"fc_08f5a80549ac3c9800691e2cb313f4819b85f889bbc12d4f91","status":"completed"},{"call_id":"call_KPprJEhF7cJMtOWar14nESCk","output":"{\"operation\":\"add\",\"a\":5,\"b\":3,\"result\":8}","type":"function_call_output"}],"tools":[{"type":"function","name":"calculate","description":"Perform + about Cloudflare''s MCP demo day"}],"type":"mcp_list_tools"},{"id":"rs_0604981435bbceda00691e2fc599a88195a7076484322a186d","summary":[],"type":"reasoning"},{"arguments":"{\"operation\":\"add\",\"a\":5,\"b\":3}","call_id":"call_CHadeTlACjhyVOw85LeUfisT","name":"calculate","type":"function_call","id":"fc_0604981435bbceda00691e2fcecc308195a7afacbe3071f3d4","status":"completed"},{"call_id":"call_CHadeTlACjhyVOw85LeUfisT","output":"{\"operation\":\"add\",\"a\":5,\"b\":3,\"result\":8}","type":"function_call_output"}],"tools":[{"type":"function","name":"calculate","description":"Perform arithmetic","parameters":{"type":"object","properties":{"operation":{"type":"string"},"a":{"type":"number"},"b":{"type":"number"}}}},{"type":"mcp","server_label":"weather","server_url":"https://demo-day.mcp.cloudflare.com/sse"}]}' headers: Accept-Encoding: @@ -273,7 +273,7 @@ http_interactions: message: OK headers: Date: - - Wed, 19 Nov 2025 20:46:48 GMT + - Wed, 19 Nov 2025 21:00:11 GMT Content-Type: - application/json Transfer-Encoding: @@ -299,18 +299,18 @@ http_interactions: Openai-Project: - PROJECT_ID X-Request-Id: - - req_0ddd0d9d12ce4075959cef66d2c5c81c + - req_3fddcc5049344a5199c6f6cc74bda8a2 Openai-Processing-Ms: - - '2549' + - '11817' X-Envoy-Upstream-Service-Time: - - '2552' + - '11821' Cf-Cache-Status: - DYNAMIC Set-Cookie: - - __cf_bm=0h51r0xUubUKzwauERdI6XJWkiJg99OXCnCKwE.9Nhw-1763585208-1.0.1.1-wxkThT5uZxbRZGaQirsI7hQYnLxmumvJkmMoaU8p.i6_icyR0tHG6vTMnAJWPUjPvWtpnSM.G4qAmBlJv8qcNtR2gqAQA8a8Cn.tIMFCF0U; - path=/; expires=Wed, 19-Nov-25 21:16:48 GMT; domain=.api.openai.com; HttpOnly; + - __cf_bm=UoNNgwKtiy5wdaM_NjbL3NdbBbR2ZZQ46c8IT1qdXZ0-1763586011-1.0.1.1-bwx3wUvCp8HemWZtdAYnDAUB4cDm0GRLInSJ0XL.FlCj4xHgr8VRLRIhfl36VUnSuIJBolK2SLH4Gqj.IeJbgkARuvdg6cdOB23Yk_DR6bU; + path=/; expires=Wed, 19-Nov-25 21:30:11 GMT; domain=.api.openai.com; HttpOnly; Secure; SameSite=None - - _cfuvid=kOCSRE9Q5scpXc5F2umRR8XlhV2_.BNe2DBNJvPnSUo-1763585208942-0.0.1.1-604800000; + - _cfuvid=jGhpHXXGG8jR1DoP4tdZCFcdh_M44sQK_LON4KX54hk-1763586011216-0.0.1.1-604800000; path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None Strict-Transport-Security: - max-age=31536000; includeSubDomains; preload @@ -319,16 +319,16 @@ http_interactions: Server: - cloudflare Cf-Ray: - - 9a128f103b7dcfa8-SJC + - 9a12a26fef314973-SJC Alt-Svc: - h3=":443"; ma=86400 body: encoding: ASCII-8BIT string: |- { - "id": "resp_08f5a80549ac3c9800691e2cb66654819b9c1c0dc1f10d9118", + "id": "resp_0604981435bbceda00691e2fcf686c81958723ab0883c9eb44", "object": "response", - "created_at": 1763585206, + "created_at": 1763585999, "status": "completed", "background": false, "billing": { @@ -342,7 +342,7 @@ http_interactions: "model": "gpt-5-2025-08-07", "output": [ { - "id": "msg_08f5a80549ac3c9800691e2cb768e8819bac854261a573efc4", + "id": "msg_0604981435bbceda00691e2fda1d408195b0efaf7fda0a9468", "type": "message", "status": "completed", "content": [ @@ -350,7 +350,7 @@ http_interactions: "type": "output_text", "annotations": [], "logprobs": [], - "text": "- 5 + 3 = 8\n- Weather: I don\u2019t have live weather access here. Tell me the city/ZIP and date (e.g., \u201ctoday in Seattle\u201d), and I can suggest likely conditions based on climate norms or show you how to check quickly." + "text": "5 + 3 = 8.\n\nFor the weather, tell me the location (city and country or ZIP/postcode) and whether you want current conditions or a forecast (and for which date/time)." } ], "role": "assistant" @@ -416,18 +416,18 @@ http_interactions: "top_p": 1.0, "truncation": "disabled", "usage": { - "input_tokens": 819, + "input_tokens": 599, "input_tokens_details": { "cached_tokens": 0 }, - "output_tokens": 61, + "output_tokens": 45, "output_tokens_details": { "reasoning_tokens": 0 }, - "total_tokens": 880 + "total_tokens": 644 }, "user": null, "metadata": {} } - recorded_at: Wed, 19 Nov 2025 20:46:48 GMT + recorded_at: Wed, 19 Nov 2025 21:00:11 GMT recorded_with: VCR 6.3.1 diff --git a/test/fixtures/vcr_cassettes/integration/open_ai/responses/common_format/mcp_test/test_agent_common_format_multiple_servers.yml b/test/fixtures/vcr_cassettes/integration/open_ai/responses/common_format/mcp_test/test_agent_common_format_multiple_servers.yml index e3113440..cedfbec0 100644 --- a/test/fixtures/vcr_cassettes/integration/open_ai/responses/common_format/mcp_test/test_agent_common_format_multiple_servers.yml +++ b/test/fixtures/vcr_cassettes/integration/open_ai/responses/common_format/mcp_test/test_agent_common_format_multiple_servers.yml @@ -47,7 +47,7 @@ http_interactions: message: OK headers: Date: - - Wed, 19 Nov 2025 20:47:01 GMT + - Wed, 19 Nov 2025 21:01:17 GMT Content-Type: - application/json Transfer-Encoding: @@ -61,7 +61,7 @@ http_interactions: X-Ratelimit-Remaining-Requests: - '14999' X-Ratelimit-Remaining-Tokens: - - '39999742' + - '39999552' X-Ratelimit-Reset-Requests: - 4ms X-Ratelimit-Reset-Tokens: @@ -73,18 +73,18 @@ http_interactions: Openai-Project: - PROJECT_ID X-Request-Id: - - req_3a6c3c814288492cb4c9bcffda99d81e + - req_2b65c32939164c9093850716443dd580 Openai-Processing-Ms: - - '12146' + - '24230' X-Envoy-Upstream-Service-Time: - - '12150' + - '24234' Cf-Cache-Status: - DYNAMIC Set-Cookie: - - __cf_bm=j2.cfQDVPxlsLwWrqPmnoLgRx6FjOvdhDAdZeNoiwws-1763585221-1.0.1.1-xMHbVgX_aU0ZBtQgMiIqXH.XN1a9SRt24.IvJaTK_G54jLIwVPqeBROke.LVRN2VAjY0eGnsP9daFP_4kSIvr6jIFCR0jOKkDvFXsM7Usgo; - path=/; expires=Wed, 19-Nov-25 21:17:01 GMT; domain=.api.openai.com; HttpOnly; + - __cf_bm=D2INndyqrWkuru74aJIEUTa.1TFXQXyy3nI7QuxUHh8-1763586077-1.0.1.1-yGRUVGCAwJzGHNx6vEi5mNQyj7UeAJRWtvRnvyhoOrk7UxjWBYjkkLvRI6dVskPVHyZmK0tUVzYZCAofGiAOL6952hwQFQCEWkL1uhKVnA4; + path=/; expires=Wed, 19-Nov-25 21:31:17 GMT; domain=.api.openai.com; HttpOnly; Secure; SameSite=None - - _cfuvid=HfhDIJwloIhF0Ex4_rIqS8SBlLrJguf2kwC9GiP7aHE-1763585221220-0.0.1.1-604800000; + - _cfuvid=ROwX.kC._rGh6fKQOAtJJ9UxQcpAxNl.RKSHPlB.1q0-1763586077510-0.0.1.1-604800000; path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None Strict-Transport-Security: - max-age=31536000; includeSubDomains; preload @@ -93,16 +93,16 @@ http_interactions: Server: - cloudflare Cf-Ray: - - 9a128f247e6715e0-SJC + - 9a12a3c09eacba62-SJC Alt-Svc: - h3=":443"; ma=86400 body: encoding: ASCII-8BIT string: |- { - "id": "resp_01142e73b6db117e00691e2cb911f88198b67e43d7dc3c2291", + "id": "resp_06a0f877661f6c2a00691e3005458c819aa2ea27e6a884bc7f", "object": "response", - "created_at": 1763585209, + "created_at": 1763586053, "status": "completed", "background": false, "billing": { @@ -116,7 +116,7 @@ http_interactions: "model": "gpt-5-2025-08-07", "output": [ { - "id": "mcpl_01142e73b6db117e00691e2cb968608198a41622b3b25afc00", + "id": "mcpl_06a0f877661f6c2a00691e30058154819ab3befe700c8577f2", "type": "mcp_list_tools", "server_label": "weather", "tools": [ @@ -134,7 +134,7 @@ http_interactions: ] }, { - "id": "mcpl_01142e73b6db117e00691e2cb968f081989cfd717a72971239", + "id": "mcpl_06a0f877661f6c2a00691e30058210819a96ef93b760a8df01", "type": "mcp_list_tools", "server_label": "github_copilot", "tools": [ @@ -1858,12 +1858,12 @@ http_interactions: ] }, { - "id": "rs_01142e73b6db117e00691e2cbb47308198b2c8f0e0c04ed9e7", + "id": "rs_06a0f877661f6c2a00691e3008481c819aa9138acd0c11ef38", "type": "reasoning", "summary": [] }, { - "id": "msg_01142e73b6db117e00691e2cc3337c8198a4606717b4ac6447", + "id": "msg_06a0f877661f6c2a00691e301b8544819a8564dee53b5a19f3", "type": "message", "status": "completed", "content": [ @@ -1871,7 +1871,7 @@ http_interactions: "type": "output_text", "annotations": [], "logprobs": [], - "text": "Happy to help! To get started, I need a couple details:\n\n- Weather:\n - Which location (city and country or ZIP/postcode)?\n - Do you want current conditions, today\u2019s forecast, or a multi\u2011day forecast?\n\n- Repository information:\n - Which GitHub repository (owner/name or a URL)?\n - What details do you want (e.g., branches, latest commits, open issues/PRs, releases, tags)?" + "text": "I\u2019m happy to do that\u2014just need a couple details:\n\n- Weather: What location (city/country or lat/long)? Do you want current conditions or a forecast, and in Celsius or Fahrenheit?\n- Repository: Which GitHub repo (owner/name or URL)? What specifics do you want (stars/forks, latest commit, open issues/PRs, branches, tags/releases, languages)?\n\nShare those and I\u2019ll fetch the info." } ], "role": "assistant" @@ -1923,16 +1923,16 @@ http_interactions: "usage": { "input_tokens": 5369, "input_tokens_details": { - "cached_tokens": 5248 + "cached_tokens": 0 }, - "output_tokens": 482, + "output_tokens": 672, "output_tokens_details": { - "reasoning_tokens": 384 + "reasoning_tokens": 576 }, - "total_tokens": 5851 + "total_tokens": 6041 }, "user": null, "metadata": {} } - recorded_at: Wed, 19 Nov 2025 20:47:01 GMT + recorded_at: Wed, 19 Nov 2025 21:01:17 GMT recorded_with: VCR 6.3.1 diff --git a/test/fixtures/vcr_cassettes/integration/open_ai/responses/common_format/mcp_test/test_agent_common_format_single_server.yml b/test/fixtures/vcr_cassettes/integration/open_ai/responses/common_format/mcp_test/test_agent_common_format_single_server.yml index c4e4aa8f..e2a98775 100644 --- a/test/fixtures/vcr_cassettes/integration/open_ai/responses/common_format/mcp_test/test_agent_common_format_single_server.yml +++ b/test/fixtures/vcr_cassettes/integration/open_ai/responses/common_format/mcp_test/test_agent_common_format_single_server.yml @@ -47,7 +47,7 @@ http_interactions: message: OK headers: Date: - - Wed, 19 Nov 2025 20:46:21 GMT + - Wed, 19 Nov 2025 21:00:53 GMT Content-Type: - application/json Transfer-Encoding: @@ -61,7 +61,7 @@ http_interactions: X-Ratelimit-Remaining-Requests: - '14999' X-Ratelimit-Remaining-Tokens: - - '39999994' + - '39999672' X-Ratelimit-Reset-Requests: - 4ms X-Ratelimit-Reset-Tokens: @@ -73,18 +73,18 @@ http_interactions: Openai-Project: - PROJECT_ID X-Request-Id: - - req_a0c5f02486ed497abb5d00386934bdd8 + - req_3c03d571859d476382d6b37b08df6a27 Openai-Processing-Ms: - - '8667' + - '13185' X-Envoy-Upstream-Service-Time: - - '8670' + - '13695' Cf-Cache-Status: - DYNAMIC Set-Cookie: - - __cf_bm=hEMCBTQW2kbtAW3WZlboz0.6poiAhEfPOmz0KuCKxBA-1763585181-1.0.1.1-6CSuuuouS3h6lNKBeSKHyL02OMvCAYjGjtPdJVJAAIVrsQ8HAk2vSUyOuXNN17MJ1eEXsBSQFV4QuH._fMIpsYxFwn.DZjjH9MjdBYkZb8o; - path=/; expires=Wed, 19-Nov-25 21:16:21 GMT; domain=.api.openai.com; HttpOnly; + - __cf_bm=kflfqHdJ1B8HHdvMcnWGiSw67yQk4AODpq80TmZIXSU-1763586053-1.0.1.1-H1uVqWqBvl.z9DDg33zvzS3PKtETnWw.A8KN0H.pNadRshogV23OcX1YKLmoInBoCtXzxDWy8EHDVBZq1kg6AeqGojBr08itByDBaFvPwUM; + path=/; expires=Wed, 19-Nov-25 21:30:53 GMT; domain=.api.openai.com; HttpOnly; Secure; SameSite=None - - _cfuvid=kWh83s9g4apTauc3btcfmRN7gISFSYbxm6AUx7mq0_c-1763585181322-0.0.1.1-604800000; + - _cfuvid=AMJuJxcHyxnJNR5ESnrTVjjFtWH5jwUltQDgI2m9PzM-1763586053133-0.0.1.1-604800000; path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None Strict-Transport-Security: - max-age=31536000; includeSubDomains; preload @@ -93,16 +93,16 @@ http_interactions: Server: - cloudflare Cf-Ray: - - 9a128e40ad027aaf-SJC + - 9a12a36a0f2dfb4c-SJC Alt-Svc: - h3=":443"; ma=86400 body: encoding: ASCII-8BIT string: |- { - "id": "resp_0a61b801b444d08000691e2c94a29c8197912046d330286604", + "id": "resp_0c6ed4b8ea47a17200691e2ff805e08194a3b62232cbe32246", "object": "response", - "created_at": 1763585172, + "created_at": 1763586040, "status": "completed", "background": false, "billing": { @@ -116,7 +116,7 @@ http_interactions: "model": "gpt-5-2025-08-07", "output": [ { - "id": "mcpl_0a61b801b444d08000691e2c94e7e48197b1c74f8cfdb06d07", + "id": "mcpl_0c6ed4b8ea47a17200691e2ff8518081949e17ad461a2dc4f7", "type": "mcp_list_tools", "server_label": "weather", "tools": [ @@ -134,12 +134,12 @@ http_interactions: ] }, { - "id": "rs_0a61b801b444d08000691e2c9698588197827178deb303ddb2", + "id": "rs_0c6ed4b8ea47a17200691e2ff9ec648194b438f387e293eaa3", "type": "reasoning", "summary": [] }, { - "id": "msg_0a61b801b444d08000691e2c9c38688197ae98d8ca7b672d05", + "id": "msg_0c6ed4b8ea47a17200691e30044d208194ba722ed82bf17365", "type": "message", "status": "completed", "content": [ @@ -147,7 +147,7 @@ http_interactions: "type": "output_text", "annotations": [], "logprobs": [], - "text": "Sure\u2014what location should I use? Please share a city and country (or ZIP/postcode or coordinates), and your preferred units (C or F)." + "text": "Sure\u2014what location do you want the current weather for? Please share a city and country (or ZIP/postcode), and your preferred units (Celsius or Fahrenheit)." } ], "role": "assistant" @@ -191,14 +191,14 @@ http_interactions: "input_tokens_details": { "cached_tokens": 0 }, - "output_tokens": 229, + "output_tokens": 552, "output_tokens_details": { - "reasoning_tokens": 192 + "reasoning_tokens": 512 }, - "total_tokens": 371 + "total_tokens": 694 }, "user": null, "metadata": {} } - recorded_at: Wed, 19 Nov 2025 20:46:21 GMT + recorded_at: Wed, 19 Nov 2025 21:00:53 GMT recorded_with: VCR 6.3.1 diff --git a/test/fixtures/vcr_cassettes/integration/open_ai/responses/common_format/mcp_test/test_agent_common_format_single_server_with_auth.yml b/test/fixtures/vcr_cassettes/integration/open_ai/responses/common_format/mcp_test/test_agent_common_format_single_server_with_auth.yml index 28b1a402..d079b251 100644 --- a/test/fixtures/vcr_cassettes/integration/open_ai/responses/common_format/mcp_test/test_agent_common_format_single_server_with_auth.yml +++ b/test/fixtures/vcr_cassettes/integration/open_ai/responses/common_format/mcp_test/test_agent_common_format_single_server_with_auth.yml @@ -47,7 +47,7 @@ http_interactions: message: OK headers: Date: - - Wed, 19 Nov 2025 20:46:12 GMT + - Wed, 19 Nov 2025 21:00:39 GMT Content-Type: - application/json Transfer-Encoding: @@ -61,7 +61,7 @@ http_interactions: X-Ratelimit-Remaining-Requests: - '14999' X-Ratelimit-Remaining-Tokens: - - '39999552' + - '39999374' X-Ratelimit-Reset-Requests: - 4ms X-Ratelimit-Reset-Tokens: @@ -73,18 +73,18 @@ http_interactions: Openai-Project: - PROJECT_ID X-Request-Id: - - req_725ba7e71fda420a94b554e9145875e4 + - req_4fe1b42247f54f5f8d08f449080c2496 Openai-Processing-Ms: - - '18277' + - '27694' X-Envoy-Upstream-Service-Time: - - '18280' + - '27697' Cf-Cache-Status: - DYNAMIC Set-Cookie: - - __cf_bm=9Y5IzrkvtruiIpBJPjA_YtnMLViW09gnhXpCFU5MIr4-1763585172-1.0.1.1-2BtyiLqLt5Tk7jGO03TuGBSmxSEf9zkw7ePAewveycnA4zEyh1aeBbIl2repFN4fTRTSnVIx4O_4R9qCbIsypuWejk9VM9Z.hxB5QueNYsE; - path=/; expires=Wed, 19-Nov-25 21:16:12 GMT; domain=.api.openai.com; HttpOnly; + - __cf_bm=u_ixvNBnE_uGRZuW0BFA_8daa2YtGUUBZM67aDfW8uc-1763586039-1.0.1.1-O72bWYpsWBa.MQ7HvDBvHuK8fz0YIrTkhtSxt87JqzBfbt0LzJFK_hsNznbqDyJ3s74cJOowjnENWg7CcGFlfqSWMNubFfDBB7JROc67IVc; + path=/; expires=Wed, 19-Nov-25 21:30:39 GMT; domain=.api.openai.com; HttpOnly; Secure; SameSite=None - - _cfuvid=XS4xf6HTx0IcH9mL6bPrTEpDK9hIAo3tN57JX3FLGBk-1763585172364-0.0.1.1-604800000; + - _cfuvid=2yzg5PAukBYR91zDzzjZzZdvTx4n341kXd55hyf_nsQ-1763586039191-0.0.1.1-604800000; path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None Strict-Transport-Security: - max-age=31536000; includeSubDomains; preload @@ -93,16 +93,16 @@ http_interactions: Server: - cloudflare Cf-Ray: - - 9a128dcc7dd0fa62-SJC + - 9a12a2bac9e4270a-SJC Alt-Svc: - h3=":443"; ma=86400 body: encoding: ASCII-8BIT string: |- { - "id": "resp_0bbb974cbbc378f500691e2c8216c0819891982946cb6cf0ad", + "id": "resp_0a1a94ff5963180600691e2fdb7c308195be8226e81298424c", "object": "response", - "created_at": 1763585154, + "created_at": 1763586011, "status": "completed", "background": false, "billing": { @@ -116,7 +116,7 @@ http_interactions: "model": "gpt-5-2025-08-07", "output": [ { - "id": "mcpl_0bbb974cbbc378f500691e2c82402c8198bdf001518e0d9a7e", + "id": "mcpl_0a1a94ff5963180600691e2fdbcd648195bf2aa0f4554487ee", "type": "mcp_list_tools", "server_label": "github_copilot", "tools": [ @@ -1840,12 +1840,12 @@ http_interactions: ] }, { - "id": "rs_0bbb974cbbc378f500691e2c841370819897cf8a919e277e6a", + "id": "rs_0a1a94ff5963180600691e2fddabbc81959c66ce6978b890ef", "type": "reasoning", "summary": [] }, { - "id": "msg_0bbb974cbbc378f500691e2c925fe4819892ab87007a8a0f3a", + "id": "msg_0a1a94ff5963180600691e2ff3904c81958f84eb49da0196a1", "type": "message", "status": "completed", "content": [ @@ -1853,7 +1853,7 @@ http_interactions: "type": "output_text", "annotations": [], "logprobs": [], - "text": "Which repository do you want info for? Please provide the owner and repo name (e.g., owner/repo) or paste the GitHub URL.\n\nAlso, what details would you like? I can fetch things like:\n- Description, license, topics, default branch\n- Stars/forks/watchers\n- Latest release/tags\n- Branches and recent commits\n- Open issues/PRs and their statuses\n- Languages and file structure" + "text": "Sure\u2014what repository do you have in mind? Please provide either:\n- The GitHub URL, or\n- owner/repo (for example: vercel/next.js)\n\nAlso, what details would you like? I can fetch any of the following:\n- Overview: description, topics, license, default branch, stars/forks/watchers, last updated\n- Activity: recent commits, contributors\n- Code: branches, tags, languages, README\n- Releases: latest release, changelog\n- Work: open issues/PRs (counts or top items), labels, milestones\n\nIf you\u2019re not sure, I can pull a quick overview by default once you share the repo." } ], "role": "assistant" @@ -1896,16 +1896,16 @@ http_interactions: "usage": { "input_tokens": 5312, "input_tokens_details": { - "cached_tokens": 5248 + "cached_tokens": 0 }, - "output_tokens": 671, + "output_tokens": 849, "output_tokens_details": { - "reasoning_tokens": 576 + "reasoning_tokens": 704 }, - "total_tokens": 5983 + "total_tokens": 6161 }, "user": null, "metadata": {} } - recorded_at: Wed, 19 Nov 2025 20:46:12 GMT + recorded_at: Wed, 19 Nov 2025 21:00:39 GMT recorded_with: VCR 6.3.1 diff --git a/test/fixtures/vcr_cassettes/integration/open_ai/responses/native_format_test/test_agent_mcp_server.yml b/test/fixtures/vcr_cassettes/integration/open_ai/responses/native_format_test/test_agent_mcp_server.yml index 088e263d..5d5c7288 100644 --- a/test/fixtures/vcr_cassettes/integration/open_ai/responses/native_format_test/test_agent_mcp_server.yml +++ b/test/fixtures/vcr_cassettes/integration/open_ai/responses/native_format_test/test_agent_mcp_server.yml @@ -47,7 +47,7 @@ http_interactions: message: OK headers: Date: - - Wed, 19 Nov 2025 20:50:30 GMT + - Wed, 19 Nov 2025 21:01:31 GMT Content-Type: - application/json Transfer-Encoding: @@ -73,18 +73,18 @@ http_interactions: Openai-Project: - PROJECT_ID X-Request-Id: - - req_f930de4b1fef42efbd671c8f50d718d5 + - req_9295f14d4dd84dc099101d59970868b8 Openai-Processing-Ms: - - '6126' + - '4649' X-Envoy-Upstream-Service-Time: - - '6130' + - '4652' Cf-Cache-Status: - DYNAMIC Set-Cookie: - - __cf_bm=CSKnilMCYDGbIJen9mOqPAZ7DhPigg8Pt9lbuzpb4C0-1763585430-1.0.1.1-elHDnP58qXxQEsSrnvucnenaNLCDZx7DPQ.RgpWQl00bHT8O.GMC2dbL7nYk0AcuEUiE0o3B_2_3Fw2X1jP5rCtTjCg9XwrOBh9NALSeSiM; - path=/; expires=Wed, 19-Nov-25 21:20:30 GMT; domain=.api.openai.com; HttpOnly; + - __cf_bm=t6Md2Z8ktst2TLkho7mrzHR.qTEsSKKfrt9BW1zmbyo-1763586091-1.0.1.1-vxuhqec9qHLE6XEsGrnq1uWrMmmrlkW7D_oK9rL8dm9hHrlehfKYXHaiLKu9lK2WcXN1hbUsQNmNeyJT9mNBjCU289KxVecnenZX5YwdTL4; + path=/; expires=Wed, 19-Nov-25 21:31:31 GMT; domain=.api.openai.com; HttpOnly; Secure; SameSite=None - - _cfuvid=UCeoHiVlR_TvsF53s39eQ1irF_vsGIxk5sC3B_i1KBM-1763585430500-0.0.1.1-604800000; + - _cfuvid=23UO7O0FsnZCexgZIpX9RBhJbaDwQqPEq9WBoSNKWD8-1763586091570-0.0.1.1-604800000; path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None Strict-Transport-Security: - max-age=31536000; includeSubDomains; preload @@ -93,16 +93,16 @@ http_interactions: Server: - cloudflare Cf-Ray: - - 9a1294661dd3eb25-SJC + - 9a12a492d923cf2e-SJC Alt-Svc: - h3=":443"; ma=86400 body: encoding: ASCII-8BIT string: |- { - "id": "resp_03973314476bd0d900691e2d905f88819bb815ddd4fd9ab654", + "id": "resp_0a7f52788c12648f00691e3026e910819aa4466498f7e2f09a", "object": "response", - "created_at": 1763585424, + "created_at": 1763586087, "status": "completed", "background": false, "billing": { @@ -116,7 +116,7 @@ http_interactions: "model": "gpt-4.1-2025-04-14", "output": [ { - "id": "mcpl_03973314476bd0d900691e2d90b224819b91340ffbd1a64e78", + "id": "mcpl_0a7f52788c12648f00691e3027380c819a8fa6e670b6533e37", "type": "mcp_list_tools", "server_label": "cloudflare-demo", "tools": [ @@ -134,7 +134,7 @@ http_interactions: ] }, { - "id": "msg_03973314476bd0d900691e2d922164819b80e5e14143fd8a75", + "id": "msg_0a7f52788c12648f00691e30289804819ab5994045994e21da", "type": "message", "status": "completed", "content": [ @@ -142,7 +142,7 @@ http_interactions: "type": "output_text", "annotations": [], "logprobs": [], - "text": "Here are the tools I currently have available:\n\n1. **Image Input Capabilities:** I can analyze and interpret images you upload.\n2. **Web Browsing (not available right now):** Sometimes, I have the ability to access the web for real-time information, but it's currently disabled.\n3. **Plugin for Cloudflare's MCP Demo Day:** \n - I can fetch information about Cloudflare\u2019s Managed Components Platform (MCP) Demo Day using a dedicated plugin.\n\nIf you need information from these areas or want to try the MCP Demo Day plugin, let me know!" + "text": "Here are the tools I have available right now:\n\n1. **Image Input Capabilities:** I can analyze and interpret images you upload.\n2. **Cloudflare MCP Demo Day Information:** I have access to a tool (`mcp_cloudflare-demo`) that allows me to fetch information specifically related to Cloudflare\u2019s MCP (Magic Cloud Platform) Demo Day.\n\nIf you have a specific question or need information using one of these tools, just let me know!" } ], "role": "assistant" @@ -186,14 +186,14 @@ http_interactions: "input_tokens_details": { "cached_tokens": 0 }, - "output_tokens": 119, + "output_tokens": 93, "output_tokens_details": { "reasoning_tokens": 0 }, - "total_tokens": 194 + "total_tokens": 168 }, "user": null, "metadata": {} } - recorded_at: Wed, 19 Nov 2025 20:50:30 GMT + recorded_at: Wed, 19 Nov 2025 21:01:31 GMT recorded_with: VCR 6.3.1 diff --git a/test/integration/open_ai/responses/common_format/mcp_test.rb b/test/integration/open_ai/responses/common_format/mcp_test.rb index ad6d4cb6..84b4f762 100644 --- a/test/integration/open_ai/responses/common_format/mcp_test.rb +++ b/test/integration/open_ai/responses/common_format/mcp_test.rb @@ -27,7 +27,7 @@ class TestAgent < ActiveAgent::Base def common_format_single_server prompt( input: "Get the current weather", - mcp_servers: [ + mcps: [ { name: "weather", url: "https://demo-day.mcp.cloudflare.com/sse" } ] ) @@ -49,7 +49,7 @@ def common_format_single_server def common_format_single_server_with_auth prompt( input: "Get repository information", - mcp_servers: [ + mcps: [ { name: "github_copilot", url: "https://api.githubcopilot.com/mcp/", authorization: ENV["GITHUB_MCP_TOKEN"] } ] ) @@ -76,7 +76,7 @@ def common_format_single_server_with_auth def common_format_multiple_servers prompt( input: "Get the weather and repository information", - mcp_servers: [ + mcps: [ { name: "weather", url: "https://demo-day.mcp.cloudflare.com/sse" }, { name: "github_copilot", url: "https://api.githubcopilot.com/mcp/", authorization: ENV["GITHUB_MCP_TOKEN"] } ] @@ -125,7 +125,7 @@ def common_format_mixed_tools_and_mcp } } ], - mcp_servers: [ + mcps: [ { name: "weather", url: "https://demo-day.mcp.cloudflare.com/sse" } ] ) From 700132ab083ca8873625d617a8be715a9b2f86e4 Mon Sep 17 00:00:00 2001 From: Zane Wolfgang Pickett Date: Wed, 19 Nov 2025 13:33:28 -0800 Subject: [PATCH 14/17] Fix bug with native Anthropic MCPs --- .../providers/anthropic/transforms.rb | 5 ++- test/providers/anthropic/transforms_test.rb | 42 ++++++++++++++++++- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/lib/active_agent/providers/anthropic/transforms.rb b/lib/active_agent/providers/anthropic/transforms.rb index a2b2aead..747ed2e0 100644 --- a/lib/active_agent/providers/anthropic/transforms.rb +++ b/lib/active_agent/providers/anthropic/transforms.rb @@ -81,8 +81,9 @@ def normalize_mcp_servers(mcp_servers) mcp_servers.map do |server| server_hash = server.is_a?(Hash) ? server.deep_symbolize_keys : server - # If already in Anthropic format (has type: "url" and authorization_token), return as-is - if server_hash[:type] == "url" && !server_hash[:authorization] + # If already in Anthropic native format (has type: "url"), return as-is + # Check for absence of common format 'authorization' field OR presence of native 'authorization_token' + if server_hash[:type] == "url" && (server_hash[:authorization_token] || !server_hash[:authorization]) next server_hash end diff --git a/test/providers/anthropic/transforms_test.rb b/test/providers/anthropic/transforms_test.rb index 737127fd..3c223a7b 100644 --- a/test/providers/anthropic/transforms_test.rb +++ b/test/providers/anthropic/transforms_test.rb @@ -587,7 +587,7 @@ def transforms assert_nil result[0][:authorization_token] end - test "normalize_mcp_servers preserves Anthropic format" do + test "normalize_mcp_servers preserves Anthropic format without auth" do mcp_servers = [ { type: "url", @@ -601,6 +601,46 @@ def transforms assert_equal 1, result.size assert_equal "url", result[0][:type] assert_equal "stripe", result[0][:name] + assert_equal "https://mcp.stripe.com", result[0][:url] + end + + test "normalize_mcp_servers preserves Anthropic format with authorization_token" do + mcp_servers = [ + { + type: "url", + name: "stripe", + url: "https://mcp.stripe.com", + authorization_token: "sk_test_123" + } + ] + + result = transforms.normalize_mcp_servers(mcp_servers) + + assert_equal 1, result.size + assert_equal "url", result[0][:type] + assert_equal "stripe", result[0][:name] + assert_equal "https://mcp.stripe.com", result[0][:url] + assert_equal "sk_test_123", result[0][:authorization_token] + end + + test "normalize_mcp_servers converts common format with authorization to native" do + mcp_servers = [ + { + type: "url", + name: "test", + url: "https://test.com", + authorization: "token123" # Common format field, should be converted + } + ] + + result = transforms.normalize_mcp_servers(mcp_servers) + + assert_equal 1, result.size + assert_equal "url", result[0][:type] + assert_equal "test", result[0][:name] + assert_equal "https://test.com", result[0][:url] + assert_equal "token123", result[0][:authorization_token] + assert_nil result[0][:authorization] # Should not have common format field end test "normalize_mcp_servers handles multiple servers" do From 7255037d348030806b1ba40a8d9367af65923a7b Mon Sep 17 00:00:00 2001 From: Zane Wolfgang Pickett Date: Wed, 19 Nov 2025 13:44:58 -0800 Subject: [PATCH 15/17] Update CHANGELOG for MCPs --- CHANGELOG.md | 82 +++++++++++++++++-------- test/docs/actions/mcps_examples_test.rb | 8 +-- 2 files changed, 62 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 71d7740a..a7bd267a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,64 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Added + +**Universal Tools Format** +```ruby +# Single format works across all providers (Anthropic, OpenAI, OpenRouter, Ollama, Mock) +tools: [{ + name: "get_weather", + description: "Get current weather", + parameters: { + type: "object", + properties: { + location: { type: "string", description: "City and state" } + }, + required: ["location"] + } +}] + +# Tool choice normalization +tool_choice: "auto" # Let model decide +tool_choice: "required" # Force tool use +tool_choice: { name: "get_weather" } # Force specific tool +``` + +Automatic conversion to provider-specific formats. Old formats still work (backward compatible). + +**Model Context Protocol (MCP) Support** +```ruby +# Universal MCP format works across providers (Anthropic, OpenAI) +class MyAgent < ActiveAgent::Base + generate_with :anthropic, model: "claude-haiku-4-5" + + def research + prompt( + message: "Research AI developments", + mcps: [{ + name: "github", + url: "https://api.githubcopilot.com/mcp/", + authorization: ENV["GITHUB_MCP_TOKEN"] + }] + ) + end +end +``` + +- Common format: `{name: "server", url: "https://...", authorization: "token"}` +- Auto-converts to provider native formats +- Anthropic: Beta API support, up to 20 servers per request +- OpenAI: Responses API with pre-built connectors (Dropbox, Google Drive, etc.) +- Backwards compatible: accepts both `mcps` and `mcp_servers` parameters +- Comprehensive documentation with tested examples +- Full VCR test coverage with real MCP endpoints + +### Changed + +- Shared `ToolChoiceClearing` concern eliminates duplication across providers + ## [1.0.0] - 2025-11-21 Major refactor with breaking changes. Complete provider rewrite. New modular architecture. @@ -111,29 +169,6 @@ Template paths: ### Added -**Universal Tools Format** -```ruby -# Single format works across all providers (Anthropic, OpenAI, OpenRouter, Ollama, Mock) -tools: [{ - name: "get_weather", - description: "Get current weather", - parameters: { - type: "object", - properties: { - location: { type: "string", description: "City and state" } - }, - required: ["location"] - } -}] - -# Tool choice normalization -tool_choice: "auto" # Let model decide -tool_choice: "required" # Force tool use -tool_choice: { name: "get_weather" } # Force specific tool -``` - -Automatic conversion to provider-specific formats. Old formats still work (backward compatible). - **Mock Provider for Testing** ```ruby class MyAgent < ActiveAgent::Base @@ -219,7 +254,6 @@ response.usage.service_tier # Anthropic - Retry logic moved to provider SDKs (automatic exponential backoff) - Migrated to official SDKs: `openai` gem and `anthropic` gem - Type-safe options with per-provider definitions -- Shared `ToolChoiceClearing` concern eliminates duplication across providers **Configuration** - Options configurable at class level, instance level, or per-call diff --git a/test/docs/actions/mcps_examples_test.rb b/test/docs/actions/mcps_examples_test.rb index 2a4d959b..fd49ea7b 100644 --- a/test/docs/actions/mcps_examples_test.rb +++ b/test/docs/actions/mcps_examples_test.rb @@ -10,7 +10,7 @@ class WeatherAgent < ActiveAgent::Base def forecast prompt( - message: "What's the weather like?", + "What's the weather like?", mcps: [ { name: "weather", url: "https://demo-day.mcp.cloudflare.com/sse" } ] ) end @@ -35,7 +35,7 @@ class DataAgent < ActiveAgent::Base def analyze prompt( - message: "Analyze the latest data", + "Analyze the latest data", mcps: [ { name: "cloudflare-demo", url: "https://demo-day.mcp.cloudflare.com/sse" } ] ) end @@ -60,7 +60,7 @@ class IntegratedAgent < ActiveAgent::Base def research prompt( - input: "Research the latest AI developments", + "Research the latest AI developments", mcps: [ { name: "cloudflare", url: "https://demo-day.mcp.cloudflare.com/sse" }, { name: "github", url: "https://api.githubcopilot.com/mcp/", authorization: ENV["GITHUB_MCP_TOKEN"] } @@ -88,7 +88,7 @@ class HybridAgent < ActiveAgent::Base def analyze_data prompt( - input: "Calculate and fetch data", + "Calculate and fetch data", tools: [ { name: "calculate", description: "Perform calculations", From 45c36bf539c06c41643fc6dc6a31819528867759 Mon Sep 17 00:00:00 2001 From: Zane Wolfgang Pickett Date: Fri, 21 Nov 2025 18:10:01 -0800 Subject: [PATCH 16/17] Fix issues extracting the correct message content from Anthropic MCP usage when in json_object mode --- .../providers/anthropic_provider.rb | 25 +- .../providers/common/messages/_types.rb | 67 ++-- .../providers/common/messages/assistant.rb | 24 +- test/providers/common/messages/types_test.rb | 331 ++++++++++++++++++ 4 files changed, 407 insertions(+), 40 deletions(-) create mode 100644 test/providers/common/messages/types_test.rb diff --git a/lib/active_agent/providers/anthropic_provider.rb b/lib/active_agent/providers/anthropic_provider.rb index 1bdb0009..3e193104 100644 --- a/lib/active_agent/providers/anthropic_provider.rb +++ b/lib/active_agent/providers/anthropic_provider.rb @@ -17,6 +17,9 @@ module Providers # # @see BaseProvider class AnthropicProvider < BaseProvider + # Lead-in message for JSON response format emulation + JSON_RESPONSE_FORMAT_LEAD_IN = "Here is the JSON requested:\n{" + # @todo Add support for Anthropic::BedrockClient and Anthropic::VertexClient # @return [Anthropic::Client] def client @@ -72,7 +75,7 @@ def prepare_prompt_request_response_format self.message_stack.push({ role: "assistant", - content: "Here is the JSON requested:\n{" + content: JSON_RESPONSE_FORMAT_LEAD_IN }) end @@ -227,7 +230,7 @@ def process_prompt_finished(api_response = nil) # # Handles JSON response format simulation by prepending `{` to the response - # content after removing the assistant lead-in message. + # content if the last message in the request is the JSON lead-in prompt. # # @see BaseProvider#process_prompt_finished_extract_messages # @param api_response [Hash] converted response hash @@ -235,10 +238,20 @@ def process_prompt_finished(api_response = nil) def process_prompt_finished_extract_messages(api_response) return unless api_response - # Handle JSON response format simulation - if request.response_format&.dig(:type) == "json_object" - request.pop_message! - api_response[:content][0][:text] = "{#{api_response[:content][0][:text]}" + # Get the last message (may be either Hash or gem object) + last_message = request.messages.last + last_role = last_message.is_a?(Hash) ? last_message[:role] : last_message&.role + last_content = last_message.is_a?(Hash) ? last_message[:content] : last_message&.content + + # Check if the last message in request is the JSON lead-in prompt + if last_role.to_sym == :assistant && last_content == JSON_RESPONSE_FORMAT_LEAD_IN + # Remove the lead-in message from the request + request.messages.pop + + # Prepend "{" to the response's first content text + if api_response[:content]&.first&.dig(:text) + api_response[:content][0][:text] = "{#{api_response[:content][0][:text]}" + end end [ api_response ] diff --git a/lib/active_agent/providers/common/messages/_types.rb b/lib/active_agent/providers/common/messages/_types.rb index 724fdde4..5c93623a 100644 --- a/lib/active_agent/providers/common/messages/_types.rb +++ b/lib/active_agent/providers/common/messages/_types.rb @@ -51,12 +51,6 @@ def cast_message(value) when "assistant" # Filter to only known attributes for Assistant filtered_hash = hash.slice(:role, :content, :name) - - # Compress content array to string if needed (Anthropic format) - if filtered_hash[:content].is_a?(Array) - filtered_hash[:content] = compress_content_array(filtered_hash[:content]) - end - Common::Messages::Assistant.new(**filtered_hash) when "tool" # Filter to only known attributes for Tool @@ -94,29 +88,6 @@ def serialize_message(value) raise ArgumentError, "Cannot serialize #{value.class}" end end - - # Compresses Anthropic-style content array into a string. - # - # Anthropic messages can have content as an array of blocks like: - # [{type: "text", text: "..."}, {type: "tool_use", ...}] - # This extracts and joins text blocks into a single string. - # - # @param content_array [Array] - # @return [String] - def compress_content_array(content_array) - content_array.map do |block| - case block[:type]&.to_s - when "text" - block[:text] - when "tool_use" - # Tool use blocks don't have readable text content - nil - else - # Unknown block type, try to extract text if present - block[:text] - end - end.compact.join("\n") - end end # Type for Messages array @@ -124,7 +95,9 @@ class MessagesType < ActiveModel::Type::Value def cast(value) case value when Array - value.map { |v| message_type.cast(v) }.compact + messages = value.map { |v| message_type.cast(v) }.compact + # Split messages with array content into separate messages + messages.flat_map { |msg| split_content_blocks(msg) } when nil [] else @@ -152,6 +125,40 @@ def deserialize(value) def message_type @message_type ||= MessageType.new end + + # Splits an assistant message with array content into separate messages + # for each content block. + # + # @param message [Common::Messages::Base] + # @return [Array] + def split_content_blocks(message) + # Only split assistant messages with array content + return [ message ] unless message.is_a?(Common::Messages::Assistant) && message.content.is_a?(Array) + + message.content.map do |block| + case block[:type]&.to_s + when "text" + # Create a message for text blocks + Common::Messages::Assistant.new(role: "assistant", content: block[:text], name: message.name) + when "tool_use" + # Create a message with tool use info as string representation + tool_info = "[Tool Use: #{block[:name]}]\nID: #{block[:id]}\nInput: #{JSON.pretty_generate(block[:input])}" + Common::Messages::Assistant.new(role: "assistant", content: tool_info, name: message.name) + when "mcp_tool_use" + # Create a message with MCP tool use info + tool_info = "[MCP Tool Use: #{block[:name]}]\nID: #{block[:id]}\nServer: #{block[:server_name]}\nInput: #{JSON.pretty_generate(block[:input] || {})}" + Common::Messages::Assistant.new(role: "assistant", content: tool_info, name: message.name) + when "mcp_tool_result" + # Create a message with MCP tool result + result_info = "[MCP Tool Result]\n#{block[:content]}" + Common::Messages::Assistant.new(role: "assistant", content: result_info, name: message.name) + else + # For unknown block types, try to extract text + content = block[:text] || block.to_s + Common::Messages::Assistant.new(role: "assistant", content:, name: message.name) + end + end.compact + end end end end diff --git a/lib/active_agent/providers/common/messages/assistant.rb b/lib/active_agent/providers/common/messages/assistant.rb index e09aa422..2099b182 100644 --- a/lib/active_agent/providers/common/messages/assistant.rb +++ b/lib/active_agent/providers/common/messages/assistant.rb @@ -9,7 +9,7 @@ module Messages # Represents messages sent by the AI assistant in a conversation. class Assistant < Base attribute :role, :string, as: "assistant" - attribute :content, :string + attribute :content # Accept both string and array (provider-native formats) attribute :name, :string validates :content, presence: true @@ -24,9 +24,16 @@ class Assistant < Base # @param normalize_names [Symbol, nil] key normalization method (e.g., :underscore) # @return [Hash, Array, nil] parsed JSON structure or nil if parsing fails def parsed_json(symbolize_names: true, normalize_names: :underscore) - start_char = [ content.index("{"), content.index("[") ].compact.min - end_char = [ content.rindex("}"), content.rindex("]") ].compact.max - content_stripped = content[start_char..end_char] if start_char && end_char + # Handle array content (from content blocks) by searching through each block + content_str = if content.is_a?(Array) + content.map { |block| block.is_a?(Hash) ? block[:text] : block.to_s }.join("\n") + else + content.to_s + end + + start_char = [ content_str.index("{"), content_str.index("[") ].compact.min + end_char = [ content_str.rindex("}"), content_str.rindex("]") ].compact.max + content_stripped = content_str[start_char..end_char] if start_char && end_char return unless content_stripped content_parsed = JSON.parse(content_stripped) @@ -48,6 +55,15 @@ def parsed_json(symbolize_names: true, normalize_names: :underscore) nil end + # Returns content as a string, handling both string and array formats + def text + if content.is_a?(Array) + content.map { |block| block.is_a?(Hash) ? block[:text] : block.to_s }.join("\n") + else + content.to_s + end + end + alias_method :json_object, :parsed_json alias_method :parse_json, :parsed_json end diff --git a/test/providers/common/messages/types_test.rb b/test/providers/common/messages/types_test.rb new file mode 100644 index 00000000..495b2b6f --- /dev/null +++ b/test/providers/common/messages/types_test.rb @@ -0,0 +1,331 @@ +# frozen_string_literal: true + +require "test_helper" +require "active_agent/providers/common/messages/_types" + +module ActiveAgent + module Providers + module Common + module Messages + class TypesTest < ActiveSupport::TestCase + test "MessageType casts strings and hashes to appropriate message types" do + message_type = create_message_type + + # String → User message + user_result = message_type.cast("Hello") + assert_instance_of ActiveAgent::Providers::Common::Messages::User, user_result + assert_equal "Hello", user_result.content + + # Hash with user role → User message + user_hash = message_type.cast({ role: "user", content: "Hi" }) + assert_instance_of ActiveAgent::Providers::Common::Messages::User, user_hash + + # Hash with assistant role → Assistant message + assistant_result = message_type.cast({ role: "assistant", content: "Hello" }) + assert_instance_of ActiveAgent::Providers::Common::Messages::Assistant, assistant_result + end + + test "MessageType drops system messages" do + message_type = create_message_type + result = message_type.cast({ role: "system", content: "System prompt" }) + assert_nil result + end + + test "MessagesType casts array of Hash messages to Message objects" do + messages_type = create_messages_type + messages = [ + { role: "user", content: "Hi" }, + { role: "assistant", content: "Hello" } + ] + result = messages_type.cast(messages) + + assert_equal 2, result.length + assert_instance_of ActiveAgent::Providers::Common::Messages::User, result[0] + assert_instance_of ActiveAgent::Providers::Common::Messages::Assistant, result[1] + end + + test "MessagesType casts nil to empty array" do + messages_type = create_messages_type + result = messages_type.cast(nil) + + assert_equal [], result + end + + test "MessagesType splits assistant message with array content into separate messages" do + messages_type = create_messages_type + messages = [ + { + role: "assistant", + content: [ + { type: "text", text: "Hello" }, + { type: "text", text: "World" } + ] + } + ] + result = messages_type.cast(messages) + + assert_equal 2, result.length + assert_all_instances_of(result, ActiveAgent::Providers::Common::Messages::Assistant) + assert_equal "Hello", result[0].content + assert_equal "World", result[1].content + end + + test "MessagesType splits tool_use blocks into separate messages" do + messages_type = create_messages_type + messages = [ + { + role: "assistant", + content: [ + { type: "text", text: "I'll help with that" }, + { + type: "tool_use", + id: "tool_123", + name: "search", + input: { query: "test" } + } + ] + } + ] + result = messages_type.cast(messages) + + assert_equal 2, result.length + assert_all_instances_of(result, ActiveAgent::Providers::Common::Messages::Assistant) + + # First message is text + assert_equal "I'll help with that", result[0].content + + # Second message contains tool info + assert_includes result[1].content, "[Tool Use: search]" + assert_includes result[1].content, "ID: tool_123" + assert_includes result[1].content, "Input:" + end + + test "MessagesType splits mcp_tool_use blocks into separate messages" do + messages_type = create_messages_type + messages = [ + { + role: "assistant", + content: [ + { type: "text", text: "Using MCP" }, + { + type: "mcp_tool_use", + id: "mcp_123", + name: "get_file", + server_name: "file_server", + input: { path: "/home/user/file.txt" } + } + ] + } + ] + result = messages_type.cast(messages) + + assert_equal 2, result.length + assert_all_instances_of(result, ActiveAgent::Providers::Common::Messages::Assistant) + + # MCP tool message + mcp_message = result[1].content + assert_includes mcp_message, "[MCP Tool Use: get_file]" + assert_includes mcp_message, "ID: mcp_123" + assert_includes mcp_message, "Server: file_server" + assert_includes mcp_message, "Input:" + end + + test "MessagesType splits mcp_tool_result blocks into separate messages" do + messages_type = create_messages_type + messages = [ + { + role: "assistant", + content: [ + { + type: "mcp_tool_result", + id: "result_123", + name: "get_file", + content: "File contents here" + } + ] + } + ] + result = messages_type.cast(messages) + + assert_equal 1, result.length + result_message = result[0].content + assert_includes result_message, "[MCP Tool Result]" + assert_includes result_message, "File contents here" + end + + test "MessagesType handles mixed content types in single message" do + messages_type = create_messages_type + messages = [ + { + role: "assistant", + content: [ + { type: "text", text: "Text 1" }, + { + type: "tool_use", + id: "tool_1", + name: "search", + input: { q: "test" } + }, + { type: "text", text: "Text 2" } + ] + } + ] + result = messages_type.cast(messages) + + assert_equal 3, result.length + assert_all_instances_of(result, ActiveAgent::Providers::Common::Messages::Assistant) + assert_equal "Text 1", result[0].content + assert_includes result[1].content, "[Tool Use: search]" + assert_equal "Text 2", result[2].content + end + + test "MessagesType handles empty and nil inputs in tool blocks" do + messages_type = create_messages_type + + # Empty input in tool_use block + empty_input_result = messages_type.cast([ + { + role: "assistant", + content: [ + { + type: "tool_use", + id: "tool_1", + name: "get_time", + input: {} + } + ] + } + ]) + assert_equal 1, empty_input_result.length + assert_includes empty_input_result[0].content, "Input: {}" + + # Nil input in mcp_tool_use block + nil_input_result = messages_type.cast([ + { + role: "assistant", + content: [ + { + type: "mcp_tool_use", + id: "mcp_1", + name: "ping", + server_name: "server", + input: nil + } + ] + } + ]) + assert_equal 1, nil_input_result.length + assert_includes nil_input_result[0].content, "Input: {}" + end + + test "MessagesType preserves message name through split" do + messages_type = create_messages_type + messages = [ + { + role: "assistant", + name: "gpt-4", + content: [ + { type: "text", text: "Text 1" }, + { type: "text", text: "Text 2" } + ] + } + ] + result = messages_type.cast(messages) + + assert_equal 2, result.length + result.each { |msg| assert_equal "gpt-4", msg.name } + end + + test "MessagesType does not split non-assistant messages with array content" do + messages_type = create_messages_type + messages = [ + { + role: "user", + content: [ { type: "text", text: "User message" } ] + } + ] + result = messages_type.cast(messages) + + assert_equal 1, result.length + assert_instance_of ActiveAgent::Providers::Common::Messages::User, result[0] + end + + test "MessagesType does not split assistant messages with string content" do + messages_type = create_messages_type + messages = [ + { + role: "assistant", + content: "Simple string content" + } + ] + result = messages_type.cast(messages) + + assert_equal 1, result.length + assert_instance_of ActiveAgent::Providers::Common::Messages::Assistant, result[0] + assert_equal "Simple string content", result[0].content + end + + test "MessagesType compacts nil messages from system roles" do + messages_type = create_messages_type + messages = [ + { role: "system", content: "System prompt" }, + { role: "user", content: "User message" }, + { role: "system", content: "Another system prompt" }, + { role: "assistant", content: "Assistant response" } + ] + result = messages_type.cast(messages) + + assert_equal 2, result.length + assert_instance_of ActiveAgent::Providers::Common::Messages::User, result[0] + assert_instance_of ActiveAgent::Providers::Common::Messages::Assistant, result[1] + end + + test "Assistant message parsed_json handles array content" do + content_array = [ + { type: "text", text: '{"name": "John", "age": 30}' }, + { type: "text", text: "Some other text" } + ] + assistant_message = ActiveAgent::Providers::Common::Messages::Assistant.new(content: content_array) + + result = assistant_message.parsed_json + + assert_not_nil result + assert_equal "John", result[:name] + assert_equal 30, result[:age] + end + + test "Assistant message text method handles string and array content" do + # String content + string_message = ActiveAgent::Providers::Common::Messages::Assistant.new(content: "Hello World") + assert_equal "Hello World", string_message.text + + # Array content + array_message = ActiveAgent::Providers::Common::Messages::Assistant.new( + content: [ + { type: "text", text: "Hello" }, + { type: "text", text: "World" } + ] + ) + assert_equal "Hello\nWorld", array_message.text + end + + private + + def create_message_type + # Access MessageType through the Types module + ActiveAgent::Providers::Common::Messages::Types::MessageType.new + end + + def create_messages_type + # Access MessagesType through the Types module + ActiveAgent::Providers::Common::Messages::Types::MessagesType.new + end + + def assert_all_instances_of(array, klass) + array.each { |item| assert_instance_of klass, item } + end + end + end + end + end +end From 0b9d681613f9abd9aee6110f18b7995026489b45 Mon Sep 17 00:00:00 2001 From: Zane Wolfgang Pickett Date: Fri, 21 Nov 2025 20:16:40 -0800 Subject: [PATCH 17/17] Automatically retry with anthropic json_object emulation --- .../providers/anthropic_provider.rb | 52 +++++++++++++++---- 1 file changed, 41 insertions(+), 11 deletions(-) diff --git a/lib/active_agent/providers/anthropic_provider.rb b/lib/active_agent/providers/anthropic_provider.rb index 3e193104..6f2498bf 100644 --- a/lib/active_agent/providers/anthropic_provider.rb +++ b/lib/active_agent/providers/anthropic_provider.rb @@ -20,6 +20,14 @@ class AnthropicProvider < BaseProvider # Lead-in message for JSON response format emulation JSON_RESPONSE_FORMAT_LEAD_IN = "Here is the JSON requested:\n{" + attr_internal :json_format_retry_count + + def initialize(kwargs = {}) + super + + self.json_format_retry_count = kwargs[:max_retries] || ::Anthropic::Client::DEFAULT_MAX_RETRIES + end + # @todo Add support for Anthropic::BedrockClient and Anthropic::VertexClient # @return [Anthropic::Client] def client @@ -49,18 +57,18 @@ def extract_used_function_names message_stack.pluck(:content).flatten.select { _1[:type] == "tool_use" }.pluck(:name) end - # Returns true if tool_choice forces any tool use (type == :any). + # Checks if tool_choice requires the model to call any tool. # - # @return [Boolean] + # @return [Boolean] true if tool_choice type is :any def tool_choice_forces_required? return false unless request.tool_choice.respond_to?(:type) request.tool_choice.type == :any end - # Returns [true, name] if tool_choice forces a specific tool (type == :tool). + # Checks if tool_choice requires a specific tool to be called. # - # @return [Array] + # @return [Array] [true, tool_name] if forcing a specific tool, [false, nil] otherwise def tool_choice_forces_specific? return [ false, nil ] unless request.tool_choice.respond_to?(:type) return [ false, nil ] unless request.tool_choice.type == :tool @@ -79,6 +87,12 @@ def prepare_prompt_request_response_format }) end + # Selects between Anthropic's stable and beta message APIs. + # + # Uses beta API when explicitly requested via anthropic_beta option or when + # using MCP servers, which require beta features. Falls back to stable API + # for standard message creation. + # # @see BaseProvider#api_prompt_executer # @return [Anthropic::Messages, Anthropic::Resources::Beta::Messages] def api_prompt_executer @@ -92,7 +106,7 @@ def api_prompt_executer # @see BaseProvider#api_response_normalize # @param api_response [Anthropic::Models::Message] - # @return [Hash] normalized response hash + # @return [Hash] def api_response_normalize(api_response) return api_response unless api_response @@ -217,23 +231,39 @@ def process_tool_call_function(api_function_call) end end - # Converts API response message to hash for message_stack. - # Converts Anthropic gem response object to hash for storage. + # Processes completed API response and handles JSON format retries. # + # When response_format is json_object and the response fails JSON validation, + # recursively retries the request to obtain well-formed JSON. + # + # @see BaseProvider#process_prompt_finished # @param api_response [Anthropic::Models::Message] # @return [Common::PromptResponse, nil] def process_prompt_finished(api_response = nil) # Convert gem object to hash so that raw_response[:usage] works api_response_hash = api_response ? Anthropic::Transforms.gem_to_hash(api_response) : nil - super(api_response_hash) + + common_response = super(api_response_hash) + + # If we failed to get the expected well formed JSON Object Response, recursively try again + if request.response_format&.dig(:type) == "json_object" && common_response.message.parsed_json.nil? && json_format_retry_count > 0 + self.json_format_retry_count -= 1 + + resolve_prompt + else + common_response + end end + # Reconstructs JSON responses that were split due to Anthropic format constraints. # - # Handles JSON response format simulation by prepending `{` to the response - # content if the last message in the request is the JSON lead-in prompt. + # Anthropic's API doesn't natively support json_object response format, so we + # simulate it by having the assistant echo a JSON lead-in ("Here is the JSON requested:\n{"), + # then send the response back for completion. This method detects and reverses + # that workaround by stripping the lead-in message and prepending "{" to the response. # # @see BaseProvider#process_prompt_finished_extract_messages - # @param api_response [Hash] converted response hash + # @param api_response [Hash] API response with content blocks # @return [Array, nil] def process_prompt_finished_extract_messages(api_response) return unless api_response