| // Licensed to the Apache Software Foundation (ASF) under one |
| // or more contributor license agreements. See the NOTICE file |
| // distributed with this work for additional information |
| // regarding copyright ownership. The ASF licenses this file |
| // to you under the Apache License, Version 2.0 (the |
| // "License"); you may not use this file except in compliance |
| // with the License. You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, |
| // software distributed under the License is distributed on an |
| // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| // KIND, either express or implied. See the License for the |
| // specific language governing permissions and limitations |
| // under the License. |
| |
| #include "service/query-profile-ai-analysis.h" |
| #include "service/query-profile-redaction.h" |
| #include "service/query-profile-parsing-tools.h" |
| |
| #include <algorithm> |
| #include <cstdint> |
| #include <cstring> |
| #include <mutex> |
| #include <string> |
| #include <string_view> |
| #include <utility> |
| #include <vector> |
| |
| #include <rapidjson/document.h> |
| |
| #include <gflags/gflags.h> |
| #include <gflags/gflags_declare.h> |
| #include <gutil/strings/substitute.h> |
| |
| #include "common/logging.h" |
| #include "exprs/ai-functions.h" |
| #include "runtime/exec-env.h" |
| #include "runtime/mem-pool.h" |
| #include "runtime/mem-tracker.h" |
| #include "udf/udf-internal.h" |
| #include "util/gflag-validator-util.h" |
| #include "util/json-util.h" |
| #include "util/parse-util.h" |
| #include "util/scope-exit-trigger.h" |
| |
| DECLARE_string(ai_model); |
| |
| static bool ValidateQueryProfileAiAnalysisSizeLimitPercent( |
| const char* flagname, const std::string& value) { |
| bool is_percent = false; |
| int64_t parsed = impala::ParseUtil::ParseMemSpec(value, &is_percent, 100); |
| if (is_percent && parsed > 0 && parsed <= 100) return true; |
| LOG(ERROR) << "Invalid value for --" << flagname << ": '" << value |
| << "'. Must be a percentage in range (0%, 100%]."; |
| return false; |
| } |
| |
| DEFINE_int64(query_profile_ai_analysis_profile_size_limit_bytes, |
| 256L * 1024L * 1024L, |
| "Maximum profile size in bytes allowed for query-profile AI analysis modules."); |
| DEFINE_validator(query_profile_ai_analysis_profile_size_limit_bytes, gt_zero); |
| |
| DEFINE_string(query_profile_ai_analysis_profile_size_limit_percent, "1%", |
| "Maximum profile size as percentage of process_mem_limit for query-profile AI " |
| "analysis modules (e.g. 5%)."); |
| DEFINE_validator(query_profile_ai_analysis_profile_size_limit_percent, |
| ValidateQueryProfileAiAnalysisSizeLimitPercent); |
| |
| DEFINE_int32(query_profile_ai_analysis_max_concurrent_runs, 3, |
| "Maximum number of concurrent query-profile AI analysis runs."); |
| DEFINE_validator(query_profile_ai_analysis_max_concurrent_runs, ge_zero); |
| |
| using impala_udf::FunctionContext; |
| using impala_udf::StringVal; |
| using rapidjson::Document; |
| using rapidjson::Value; |
| using namespace std; |
| |
| namespace impala { |
| |
| static constexpr int MAX_AGENT_ITERATIONS = 12; |
| static constexpr size_t MAX_TOOL_RESULT_CHARS = 100000; |
| static mutex num_profiles_being_analyzed_lock; |
| static int32_t num_profiles_being_analyzed = 0; |
| static constexpr const char JSON_KEY_ARGUMENTS[] = "arguments"; |
| static constexpr const char JSON_KEY_CHOICES[] = "choices"; |
| static constexpr const char JSON_KEY_CONTENT[] = "content"; |
| static constexpr const char JSON_KEY_ERROR[] = "error"; |
| static constexpr const char JSON_KEY_FUNCTION[] = "function"; |
| static constexpr const char JSON_KEY_ID[] = "id"; |
| static constexpr const char JSON_KEY_MESSAGE[] = "message"; |
| static constexpr const char JSON_KEY_MESSAGES[] = "messages"; |
| static constexpr const char JSON_KEY_MODEL[] = "model"; |
| static constexpr const char JSON_KEY_NAME[] = "name"; |
| static constexpr const char JSON_KEY_OUTPUT_TEXT[] = "output_text"; |
| static constexpr const char JSON_KEY_PARALLEL_TOOL_CALLS[] = "parallel_tool_calls"; |
| static constexpr const char JSON_KEY_PAYLOAD[] = "payload"; |
| static constexpr const char JSON_KEY_RESULT[] = "result"; |
| static constexpr const char JSON_KEY_ROLE[] = "role"; |
| static constexpr const char JSON_KEY_SUMMARY[] = "summary"; |
| static constexpr const char JSON_KEY_TEXT[] = "text"; |
| static constexpr const char JSON_KEY_TOOL_CALL_ID[] = "tool_call_id"; |
| static constexpr const char JSON_KEY_TOOL_CALLS[] = "tool_calls"; |
| static constexpr const char JSON_KEY_TOOL_CHOICE[] = "tool_choice"; |
| static constexpr const char JSON_KEY_TOOL_NAME[] = "tool_name"; |
| static constexpr const char JSON_KEY_TOOLS[] = "tools"; |
| static constexpr const char JSON_KEY_VALUE[] = "value"; |
| |
| static constexpr const char ROLE_ASSISTANT[] = "assistant"; |
| static constexpr const char ROLE_SYSTEM[] = "system"; |
| static constexpr const char ROLE_TOOL[] = "tool"; |
| static constexpr const char ROLE_USER[] = "user"; |
| static constexpr const char TOOL_CHOICE_AUTO[] = "auto"; |
| |
| // Attempts to reserve one slot for a profile AI analysis run. |
| static bool TryAcquireAiAnalysisSlot() { |
| lock_guard<mutex> l(num_profiles_being_analyzed_lock); |
| if (num_profiles_being_analyzed |
| >= FLAGS_query_profile_ai_analysis_max_concurrent_runs) { |
| return false; |
| } |
| ++num_profiles_being_analyzed; |
| return true; |
| } |
| |
| static void ReleaseAiAnalysisSlot() { |
| lock_guard<mutex> l(num_profiles_being_analyzed_lock); |
| if (num_profiles_being_analyzed <= 0) { |
| DCHECK_GT(num_profiles_being_analyzed, 0); |
| return; |
| } |
| --num_profiles_being_analyzed; |
| } |
| |
| // Computes the profile size limit passed to profile redaction/parsing: |
| // min(configured byte limit, configured percentage of process_mem_limit). |
| static Status ComputeProfileAnalysisSizeLimitBytes(int64_t* profile_size_limit_bytes) { |
| if (profile_size_limit_bytes == nullptr) { |
| return Status("profile size limit output pointer cannot be null"); |
| } |
| const int64_t byte_limit = FLAGS_query_profile_ai_analysis_profile_size_limit_bytes; |
| |
| ExecEnv* exec_env = ExecEnv::GetInstance(); |
| if (exec_env == nullptr || exec_env->process_mem_tracker() == nullptr) { |
| *profile_size_limit_bytes = byte_limit; |
| return Status::OK(); |
| } |
| const int64_t process_mem_limit = exec_env->process_mem_tracker()->limit(); |
| if (process_mem_limit <= 0) { |
| *profile_size_limit_bytes = byte_limit; |
| return Status::OK(); |
| } |
| |
| bool is_percent = false; |
| int64_t percent_limit_bytes = ParseUtil::ParseMemSpec( |
| FLAGS_query_profile_ai_analysis_profile_size_limit_percent, &is_percent, |
| process_mem_limit); |
| if (!is_percent || percent_limit_bytes <= 0) { |
| return Status(strings::Substitute( |
| "Invalid --query_profile_ai_analysis_profile_size_limit_percent: '$0'", |
| FLAGS_query_profile_ai_analysis_profile_size_limit_percent)); |
| } |
| *profile_size_limit_bytes = min(byte_limit, percent_limit_bytes); |
| return Status::OK(); |
| } |
| |
| static constexpr char SYSTEM_INSTRUCTION[] = |
| "You are an Impala engineer who is analyzing a query profile to identify " |
| "performance bottlenecks. Out of the potential query bottleneck sources " |
| "identified, prove or disprove each one using evidence from the Impala Query " |
| "Profile until you are left with the only answer that is plausible. Never make " |
| "any assumptions when thinking, instead always make a hypothesis and confirm it " |
| "using hard evidence before moving on. Drill down into profile details as needed " |
| "and feel free to perform multiple rounds of tool calls. Avoid finalizing root " |
| "cause from overview/summary tools alone. Ensure that the plan is optimal for " |
| "the given query. Ensure that the statistics are computed and available. " |
| "Evaluate any sources of bad memory and cardinality estimates and consider them " |
| "during overall bottleneck analysis. Your response should include all the " |
| "hypotheses you create along the way along with a piece of evidence from the profile " |
| "that justifies why each hypothesis and your final conclusion was true or false." |
| "\n\n" |
| "Use native tool calling for every non-final step. If you need more " |
| "evidence, you MUST return tool_calls (not prose) and you may include " |
| "multiple independent tool calls in the same response. Do not describe " |
| "intended tool calls in text. Only provide final plain-text " |
| "analysis (not Markdown) when no additional tools are needed."; |
| |
| static const char* ANALYSIS_PROMPT = "Summarize this Impala Query Profile, identify any " |
| "performance bottlenecks, and identify " |
| "the sources of these bottlenecks."; |
| |
| // OpenAI-style tool definitions |
| static constexpr char PROFILE_TOOLS_JSON[] = R"PT([ |
| { |
| "type": "function", |
| "function": { |
| "name": "get_summary", |
| "description": "Get the Summary section of the Impala query profile, )PT" |
| R"PT(including query metadata, options, and top-level fields.", |
| "parameters": { |
| "type": "object", |
| "properties": {}, |
| "required": [] |
| } |
| } |
| }, |
| { |
| "type": "function", |
| "function": { |
| "name": "get_timeline", |
| "description": "Get the Query Timeline section detailing the execution schedule.", |
| "parameters": { |
| "type": "object", |
| "properties": {}, |
| "required": [] |
| } |
| } |
| }, |
| { |
| "type": "function", |
| "function": { |
| "name": "get_compilation", |
| "description": "Get the Query Compilation section timeline.", |
| "parameters": { |
| "type": "object", |
| "properties": {}, |
| "required": [] |
| } |
| } |
| }, |
| { |
| "type": "function", |
| "function": { |
| "name": "get_execution_profile", |
| "description": "Get the complete Execution Profile section including )PT" |
| R"PT(per-node profiles.", |
| "parameters": { |
| "type": "object", |
| "properties": {}, |
| "required": [] |
| } |
| } |
| }, |
| { |
| "type": "function", |
| "function": { |
| "name": "get_fragments_overview", |
| "description": "Get a condensed array of all fragments and their )PT" |
| R"PT(execution summaries to triage bottlenecks.", |
| "parameters": { |
| "type": "object", |
| "properties": {}, |
| "required": [] |
| } |
| } |
| }, |
| { |
| "type": "function", |
| "function": { |
| "name": "get_fragment", |
| "description": "Get ONLY the 'Averaged Fragment' section for a )PT" |
| R"PT(specific fragment. Best for high-level analysis of a fragment's operators.", |
| "parameters": { |
| "type": "object", |
| "properties": { |
| "fragment_id": { |
| "type": "string", |
| "description": "Fragment ID to retrieve (MUST be a string, e.g., 'F00', '03')" |
| } |
| }, |
| "required": [ |
| "fragment_id" |
| ] |
| } |
| } |
| }, |
| { |
| "type": "function", |
| "function": { |
| "name": "get_all_fragments", |
| "description": "Get all block variants (Averaged, Coordinator, and )PT" |
| R"PT(standard Fragment blocks) for a fragment_id.", |
| "parameters": { |
| "type": "object", |
| "properties": { |
| "fragment_id": { |
| "type": "string", |
| "description": "Fragment ID to retrieve (MUST be a string, e.g., 'F00', '03')" |
| } |
| }, |
| "required": [ |
| "fragment_id" |
| ] |
| } |
| } |
| }, |
| { |
| "type": "function", |
| "function": { |
| "name": "get_fragment_instances", |
| "description": "Get the individual backend execution 'Instance' )PT" |
| R"PT(blocks for a specific fragment. Use this to look for data skew or )PT" |
| R"PT(straggler nodes within a fragment.", |
| "parameters": { |
| "type": "object", |
| "properties": { |
| "fragment_id": { |
| "type": "string", |
| "description": "Fragment ID to retrieve instances for (MUST be a )PT" |
| R"PT(string, e.g., 'F00', '03')" |
| } |
| }, |
| "required": [ |
| "fragment_id" |
| ] |
| } |
| } |
| }, |
| { |
| "type": "function", |
| "function": { |
| "name": "get_operator", |
| "description": "Fuzzy search to find an operator by node_id. )PT" |
| R"PT(Returns the best match found in the profile.", |
| "parameters": { |
| "type": "object", |
| "properties": { |
| "node_id": { |
| "type": "string", |
| "description": "Numeric Node ID to retrieve (MUST be a string, )PT" |
| R"PT(e.g., '00', '04', '11')" |
| }, |
| "fragment_id": { |
| "type": "string", |
| "description": "Optional Fragment ID to scope the search (e.g., 'F03')" |
| } |
| }, |
| "required": [ |
| "node_id" |
| ] |
| } |
| } |
| }, |
| { |
| "type": "function", |
| "function": { |
| "name": "get_average_operator", |
| "description": "Get ONLY the averaged metric block for a specific )PT" |
| R"PT(operator across all hosts. This is the recommended primary tool for )PT" |
| R"PT(analyzing operator performance.", |
| "parameters": { |
| "type": "object", |
| "properties": { |
| "node_id": { |
| "type": "string", |
| "description": "Numeric Node ID to retrieve (MUST be a string, )PT" |
| R"PT(e.g., '00', '04', '11')" |
| } |
| }, |
| "required": [ |
| "node_id" |
| ] |
| } |
| } |
| }, |
| { |
| "type": "function", |
| "function": { |
| "name": "get_all_operators", |
| "description": "Get an array of operator metrics from EVERY )PT" |
| R"PT(individual instance executing this node_id.", |
| "parameters": { |
| "type": "object", |
| "properties": { |
| "node_id": { |
| "type": "string", |
| "description": "Numeric Node ID to retrieve (MUST be a )PT" |
| R"PT(string, e.g., '00', '04')" |
| }, |
| "fragment_id": { |
| "type": "string", |
| "description": "Fragment ID the node belongs to (MUST be a )PT" |
| R"PT(string, e.g., 'F03')" |
| } |
| }, |
| "required": [ |
| "node_id", |
| "fragment_id" |
| ] |
| } |
| } |
| }, |
| { |
| "type": "function", |
| "function": { |
| "name": "get_specific_operator", |
| "description": "Get operator metrics for one exact backend )PT" |
| R"PT(instance to diagnose a straggler.", |
| "parameters": { |
| "type": "object", |
| "properties": { |
| "node_id": { |
| "type": "string", |
| "description": "Numeric Node ID to retrieve (MUST be a )PT" |
| R"PT(string, e.g., '00', '04')" |
| }, |
| "fragment_id": { |
| "type": "string", |
| "description": "Fragment ID to scope search (MUST be a string, e.g., 'F03')" |
| }, |
| "instance_id": { |
| "type": "string", |
| "description": "Full Instance identifier (e.g., )PT" |
| R"PT('4a4f3a5058de2282:4959cbc300000005')" |
| } |
| }, |
| "required": [ |
| "node_id", |
| "fragment_id", |
| "instance_id" |
| ] |
| } |
| } |
| }, |
| { |
| "type": "function", |
| "function": { |
| "name": "get_scan_nodes_summary", |
| "description": "Get a condensed summary of all SCAN nodes (HDFS, )PT" |
| R"PT(S3, KUDU) in the query.", |
| "parameters": { |
| "type": "object", |
| "properties": {}, |
| "required": [] |
| } |
| } |
| }, |
| { |
| "type": "function", |
| "function": { |
| "name": "get_per_node_profiles", |
| "description": "Get the Per Node Profiles section containing )PT" |
| R"PT(host-level resource utilization.", |
| "parameters": { |
| "type": "object", |
| "properties": {}, |
| "required": [] |
| } |
| } |
| }, |
| { |
| "type": "function", |
| "function": { |
| "name": "get_query_options", |
| "description": "Get parsed key/value pairs of the query options )PT" |
| R"PT((e.g. MT_DOP, RUNTIME_FILTERS) used.", |
| "parameters": { |
| "type": "object", |
| "properties": {}, |
| "required": [] |
| } |
| } |
| }, |
| { |
| "type": "function", |
| "function": { |
| "name": "get_tables_queried", |
| "description": "Get an array of the fully qualified tables queried.", |
| "parameters": { |
| "type": "object", |
| "properties": {}, |
| "required": [] |
| } |
| } |
| }, |
| { |
| "type": "function", |
| "function": { |
| "name": "get_resource_estimates", |
| "description": "Get peak memory and resource estimation metadata from the profile.", |
| "parameters": { |
| "type": "object", |
| "properties": {}, |
| "required": [] |
| } |
| } |
| }, |
| { |
| "type": "function", |
| "function": { |
| "name": "get_analyzed_query", |
| "description": "Get the expanded, analyzed SQL query text )PT" |
| R"PT(containing rewritten joins and predicates.", |
| "parameters": { |
| "type": "object", |
| "properties": {}, |
| "required": [] |
| } |
| } |
| } |
| ])PT"; |
| |
| struct ChatMessage { |
| string role; |
| string content; |
| string tool_call_id; |
| string tool_calls_json; |
| |
| ChatMessage() = default; |
| ChatMessage(string role, string content, string tool_call_id, string tool_calls_json) |
| : role(move(role)), |
| content(move(content)), |
| tool_call_id(move(tool_call_id)), |
| tool_calls_json(move(tool_calls_json)) {} |
| }; |
| |
| // Truncates oversized tool output so model payloads stay bounded. |
| static void TruncateToolOutputIfNeeded(string* tool_json) { |
| DCHECK(tool_json != nullptr); |
| if (tool_json->size() <= MAX_TOOL_RESULT_CHARS) return; |
| tool_json->resize(MAX_TOOL_RESULT_CHARS); |
| tool_json->append("\n... [TRUNCATED]"); |
| } |
| |
| static Value MakeJsonStringValue(string_view value, Document::AllocatorType& alloc) { |
| Value json_value; |
| json_value.SetString( |
| value.data(), static_cast<rapidjson::SizeType>(value.size()), alloc); |
| return json_value; |
| } |
| |
| static const Document& GetParsedProfileToolsDocument() { |
| static const Document parsed_tools_doc = [] { |
| Document doc; |
| doc.Parse(PROFILE_TOOLS_JSON, |
| static_cast<rapidjson::SizeType>(sizeof(PROFILE_TOOLS_JSON) - 1)); |
| return doc; |
| }(); |
| return parsed_tools_doc; |
| } |
| |
| // Parses a model JSON response and tolerates non-JSON prefixes/suffixes. |
| static bool ParseJsonObjectFromResponse(string_view response, Document* response_doc) { |
| DCHECK(response_doc != nullptr); |
| if (response_doc == nullptr) return false; |
| response_doc->Parse(response.data(), response.size()); |
| if (!response_doc->HasParseError() && response_doc->IsObject()) return true; |
| |
| const size_t first_brace = response.find('{'); |
| const size_t last_brace = response.rfind('}'); |
| if (first_brace == string_view::npos || last_brace == string_view::npos |
| || last_brace <= first_brace) { |
| return false; |
| } |
| const string_view candidate = |
| response.substr(first_brace, last_brace - first_brace + 1); |
| response_doc->Parse(candidate.data(), candidate.size()); |
| return !response_doc->HasParseError() && response_doc->IsObject(); |
| } |
| |
| // Returns the assistant message object from common response envelope shapes. |
| static const Value* FindAssistantMessageInResponse(const Document& response_doc) { |
| if (response_doc.HasMember(JSON_KEY_ROLE) && response_doc[JSON_KEY_ROLE].IsString()) { |
| return &response_doc; |
| } |
| if (response_doc.HasMember(JSON_KEY_TOOL_CALLS) |
| || response_doc.HasMember(JSON_KEY_CONTENT)) { |
| return &response_doc; |
| } |
| if (response_doc.HasMember(JSON_KEY_MESSAGE) |
| && response_doc[JSON_KEY_MESSAGE].IsObject()) { |
| return &response_doc[JSON_KEY_MESSAGE]; |
| } |
| if (response_doc.HasMember(JSON_KEY_CHOICES) |
| && response_doc[JSON_KEY_CHOICES].IsArray()) { |
| const Value& choices = response_doc[JSON_KEY_CHOICES]; |
| if (!choices.Empty() && choices[0].IsObject() |
| && choices[0].HasMember(JSON_KEY_MESSAGE) |
| && choices[0][JSON_KEY_MESSAGE].IsObject()) { |
| return &choices[0][JSON_KEY_MESSAGE]; |
| } |
| } |
| return nullptr; |
| } |
| |
| // Extracts assistant content text from string or content-part array formats. |
| static bool TryExtractAssistantContentText( |
| const Value* assistant_message, string* assistant_content) { |
| DCHECK(assistant_content != nullptr); |
| if (assistant_message == nullptr) return false; |
| if (assistant_content == nullptr) return false; |
| assistant_content->clear(); |
| if (!assistant_message->HasMember(JSON_KEY_CONTENT)) return false; |
| |
| const Value& content = (*assistant_message)[JSON_KEY_CONTENT]; |
| if (content.IsString()) { |
| assistant_content->assign(content.GetString(), content.GetStringLength()); |
| return true; |
| } |
| if (!content.IsArray()) return false; |
| |
| for (rapidjson::SizeType i = 0; i < content.Size(); ++i) { |
| const Value& part = content[i]; |
| if (!part.IsObject()) continue; |
| |
| const Value* text_field = nullptr; |
| if (part.HasMember(JSON_KEY_TEXT) && part[JSON_KEY_TEXT].IsString()) { |
| text_field = &part[JSON_KEY_TEXT]; |
| } else if (part.HasMember(JSON_KEY_OUTPUT_TEXT) |
| && part[JSON_KEY_OUTPUT_TEXT].IsString()) { |
| text_field = &part[JSON_KEY_OUTPUT_TEXT]; |
| } else if (part.HasMember(JSON_KEY_TEXT) && part[JSON_KEY_TEXT].IsObject() |
| && part[JSON_KEY_TEXT].HasMember(JSON_KEY_VALUE) |
| && part[JSON_KEY_TEXT][JSON_KEY_VALUE].IsString()) { |
| text_field = &part[JSON_KEY_TEXT][JSON_KEY_VALUE]; |
| } |
| if (text_field == nullptr) continue; |
| if (!assistant_content->empty()) assistant_content->append("\n"); |
| assistant_content->append(text_field->GetString(), text_field->GetStringLength()); |
| } |
| return !assistant_content->empty(); |
| } |
| |
| namespace test { |
| |
| bool ParseJsonObjectFromResponse( |
| string_view response, Document* response_doc) { |
| return impala::ParseJsonObjectFromResponse(response, response_doc); |
| } |
| |
| const Value* FindAssistantMessageInResponse(const Document& response_doc) { |
| return impala::FindAssistantMessageInResponse(response_doc); |
| } |
| |
| bool TryExtractAssistantContentText( |
| const Value* assistant_message, string* assistant_content) { |
| return impala::TryExtractAssistantContentText(assistant_message, assistant_content); |
| } |
| |
| bool TryAcquireAiAnalysisSlot() { |
| return impala::TryAcquireAiAnalysisSlot(); |
| } |
| |
| void ReleaseAiAnalysisSlot() { |
| impala::ReleaseAiAnalysisSlot(); |
| } |
| |
| } // namespace test |
| |
| // Builds the model request payload from the current conversation state. |
| static Status BuildAgentPayloadJson( |
| const vector<ChatMessage>& conversation, string* payload_json) { |
| if (payload_json == nullptr) return Status("payload output pointer cannot be null"); |
| |
| const Document& parsed_tools_doc = GetParsedProfileToolsDocument(); |
| if (parsed_tools_doc.HasParseError() || !parsed_tools_doc.IsArray()) { |
| return Status("Failed to parse PROFILE_TOOLS_JSON for AI analysis"); |
| } |
| |
| Document payload_doc; |
| payload_doc.SetObject(); |
| Document::AllocatorType& alloc = payload_doc.GetAllocator(); |
| payload_doc.AddMember(JSON_KEY_MODEL, Value(FLAGS_ai_model.c_str(), alloc), alloc); |
| Value tools_value(parsed_tools_doc, alloc); |
| payload_doc.AddMember(JSON_KEY_TOOLS, tools_value, alloc); |
| payload_doc.AddMember(JSON_KEY_TOOL_CHOICE, TOOL_CHOICE_AUTO, alloc); |
| payload_doc.AddMember(JSON_KEY_PARALLEL_TOOL_CALLS, true, alloc); |
| |
| Value messages(rapidjson::kArrayType); |
| { |
| Value system_msg(rapidjson::kObjectType); |
| system_msg.AddMember(JSON_KEY_ROLE, ROLE_SYSTEM, alloc); |
| system_msg.AddMember( |
| JSON_KEY_CONTENT, rapidjson::StringRef(SYSTEM_INSTRUCTION), alloc); |
| messages.PushBack(system_msg, alloc); |
| } |
| |
| for (const ChatMessage& msg : conversation) { |
| string_view role = msg.role; |
| if (role != ROLE_USER && role != ROLE_ASSISTANT |
| && role != ROLE_SYSTEM && role != ROLE_TOOL) { |
| role = ROLE_USER; |
| } |
| Value json_msg(rapidjson::kObjectType); |
| json_msg.AddMember(JSON_KEY_ROLE, MakeJsonStringValue(role, alloc), alloc); |
| if (role == ROLE_ASSISTANT && !msg.tool_calls_json.empty()) { |
| Document tool_calls_doc; |
| tool_calls_doc.Parse(msg.tool_calls_json.c_str()); |
| if (!tool_calls_doc.HasParseError() && tool_calls_doc.IsArray()) { |
| Value tool_calls_value(tool_calls_doc, alloc); |
| json_msg.AddMember(JSON_KEY_TOOL_CALLS, tool_calls_value, alloc); |
| } |
| if (msg.content.empty()) { |
| Value null_content; |
| null_content.SetNull(); |
| json_msg.AddMember(JSON_KEY_CONTENT, null_content, alloc); |
| } else { |
| json_msg.AddMember( |
| JSON_KEY_CONTENT, MakeJsonStringValue(msg.content, alloc), alloc); |
| } |
| } else { |
| json_msg.AddMember( |
| JSON_KEY_CONTENT, MakeJsonStringValue(msg.content, alloc), alloc); |
| if (role == ROLE_TOOL && !msg.tool_call_id.empty()) { |
| json_msg.AddMember( |
| JSON_KEY_TOOL_CALL_ID, MakeJsonStringValue(msg.tool_call_id, alloc), alloc); |
| } |
| } |
| messages.PushBack(json_msg, alloc); |
| } |
| payload_doc.AddMember(JSON_KEY_MESSAGES, messages, alloc); |
| |
| *payload_json = JsonToString(payload_doc); |
| return Status::OK(); |
| } |
| |
| // Calls ai_generate_text with the provided serialized options payload. |
| Status CallAiGenerateText(string_view impala_options_json, string* response) { |
| if (response == nullptr) return Status("response output pointer cannot be null"); |
| MemTracker tracker(-1, "QueryProfileAiAnalysis", |
| ExecEnv::GetInstance()->process_mem_tracker()); |
| MemPool udf_pool(&tracker); |
| MemPool result_pool(&tracker); |
| const auto cleanup = MakeScopeExitTrigger([&]() { |
| result_pool.FreeAll(); |
| udf_pool.FreeAll(); |
| tracker.Close(); |
| }); |
| FunctionContext::TypeDesc str_type; |
| str_type.type = FunctionContext::TYPE_STRING; |
| str_type.precision = 0; |
| str_type.scale = 0; |
| str_type.len = 0; |
| vector<FunctionContext::TypeDesc> arg_types(7, str_type); |
| FunctionContext* ctx = FunctionContextImpl::CreateContext( |
| nullptr, &udf_pool, &result_pool, str_type, arg_types); |
| if (ctx == nullptr) { |
| return Status("Failed to create function context for AI analysis"); |
| } |
| const auto ctx_cleanup = MakeScopeExitTrigger([&]() { |
| ctx->impl()->Close(); |
| delete ctx; |
| }); |
| |
| const StringVal null_arg = StringVal::null(); |
| StringVal impala_options_val(reinterpret_cast<uint8_t*>( |
| const_cast<char*>(impala_options_json.data())), |
| impala_options_json.size()); |
| StringVal result = AiFunctions::AiGenerateText( |
| ctx, null_arg, null_arg, null_arg, null_arg, null_arg, impala_options_val); |
| if (ctx->has_error()) { |
| string err = |
| ctx->error_msg() == nullptr ? "Unknown AI function error" : ctx->error_msg(); |
| return Status(err); |
| } |
| if (result.is_null) { |
| return Status("AI function returned null response"); |
| } |
| response->assign(reinterpret_cast<char*>(result.ptr), result.len); |
| return Status::OK(); |
| } |
| |
| // Runs an iterative tool-calling loop until a final analysis response is produced. |
| Status RunAiAgentWithToolExecutor(string_view initial_summary_json, |
| const AiToolExecutor& tool_executor, string* analysis) { |
| if (analysis == nullptr) return Status("analysis output pointer cannot be null"); |
| if (!tool_executor) return Status("tool executor cannot be empty"); |
| |
| vector<ChatMessage> conversation; |
| conversation.reserve(MAX_AGENT_ITERATIONS * 2); |
| string user_message; |
| user_message.reserve( |
| strlen(ANALYSIS_PROMPT) + strlen("\n\nInitial profile context:\n") |
| + initial_summary_json.size()); |
| user_message.append(ANALYSIS_PROMPT) |
| .append("\n\nInitial profile context:\n") |
| .append(initial_summary_json.data(), initial_summary_json.size()); |
| conversation.emplace_back(ROLE_USER, move(user_message), "", ""); |
| |
| string last_response; |
| for (int iteration = 0; iteration < MAX_AGENT_ITERATIONS; ++iteration) { |
| string payload_json; |
| RETURN_IF_ERROR(BuildAgentPayloadJson(conversation, &payload_json)); |
| |
| Document impala_options_doc; |
| impala_options_doc.SetObject(); |
| Document::AllocatorType& impala_options_alloc = impala_options_doc.GetAllocator(); |
| impala_options_doc.AddMember(JSON_KEY_PAYLOAD, |
| rapidjson::StringRef(payload_json.c_str(), |
| static_cast<rapidjson::SizeType>(payload_json.size())), |
| impala_options_alloc); |
| const string impala_options_json = JsonToString(impala_options_doc); |
| |
| string model_response; |
| RETURN_IF_ERROR(CallAiGenerateText(impala_options_json, &model_response)); |
| last_response = move(model_response); |
| |
| Document response_doc; |
| if (ParseJsonObjectFromResponse(last_response, &response_doc)) { |
| const Value* assistant_message = FindAssistantMessageInResponse(response_doc); |
| if (assistant_message != nullptr |
| && assistant_message->HasMember(JSON_KEY_TOOL_CALLS) |
| && (*assistant_message)[JSON_KEY_TOOL_CALLS].IsArray()) { |
| const Value& tool_calls = (*assistant_message)[JSON_KEY_TOOL_CALLS]; |
| if (tool_calls.Empty()) { |
| string assistant_content; |
| if (TryExtractAssistantContentText(assistant_message, &assistant_content)) { |
| conversation.emplace_back(ROLE_ASSISTANT, assistant_content, "", ""); |
| *analysis = move(assistant_content); |
| } else { |
| *analysis = last_response; |
| } |
| return Status::OK(); |
| } |
| |
| string assistant_content; |
| TryExtractAssistantContentText(assistant_message, &assistant_content); |
| conversation.emplace_back(ROLE_ASSISTANT, move(assistant_content), "", |
| JsonToString(tool_calls)); |
| int executed_tool_calls = 0; |
| for (rapidjson::SizeType i = 0; i < tool_calls.Size(); ++i) { |
| const Value& tool_call = tool_calls[i]; |
| if (!tool_call.IsObject()) continue; |
| if (!tool_call.HasMember(JSON_KEY_FUNCTION) |
| || !tool_call[JSON_KEY_FUNCTION].IsObject()) { |
| continue; |
| } |
| const Value& function = tool_call[JSON_KEY_FUNCTION]; |
| if (!function.HasMember(JSON_KEY_NAME) || !function[JSON_KEY_NAME].IsString()) { |
| continue; |
| } |
| |
| const Value& tool_name_value = function[JSON_KEY_NAME]; |
| const string_view tool_name( |
| tool_name_value.GetString(), tool_name_value.GetStringLength()); |
| string_view args_json; |
| if (function.HasMember(JSON_KEY_ARGUMENTS) |
| && function[JSON_KEY_ARGUMENTS].IsString()) { |
| const Value& arguments = function[JSON_KEY_ARGUMENTS]; |
| args_json = string_view(arguments.GetString(), arguments.GetStringLength()); |
| } |
| |
| string tool_call_id; |
| if (tool_call.HasMember(JSON_KEY_ID) && tool_call[JSON_KEY_ID].IsString()) { |
| const Value& id = tool_call[JSON_KEY_ID]; |
| tool_call_id.assign(id.GetString(), id.GetStringLength()); |
| } |
| if (tool_call_id.empty()) { |
| LOG(WARNING) << "Skipping tool call '" << tool_name |
| << "' because model response is missing tool_call id."; |
| string retry_message = strings::Substitute( |
| "The previous tool call for '$0' was ignored because tool_call id " |
| "was missing. Retry with a valid tool_call id.", |
| StringPiece(tool_name.data(), static_cast<int>(tool_name.size()))); |
| conversation.emplace_back(ROLE_USER, move(retry_message), "", ""); |
| continue; |
| } |
| |
| string tool_json; |
| Status tool_status = tool_executor(tool_name, args_json, &tool_json); |
| if (!tool_status.ok()) { |
| LOG(WARNING) << "Tool call '" << tool_name << "' failed: " |
| << tool_status.GetDetail(); |
| Document tool_error_doc; |
| tool_error_doc.SetObject(); |
| Document::AllocatorType& error_alloc = tool_error_doc.GetAllocator(); |
| tool_error_doc.AddMember( |
| JSON_KEY_ERROR, Value(tool_status.GetDetail().c_str(), error_alloc), |
| error_alloc); |
| tool_json = JsonToString(tool_error_doc); |
| } |
| TruncateToolOutputIfNeeded(&tool_json); |
| conversation.emplace_back(ROLE_TOOL, move(tool_json), move(tool_call_id), ""); |
| ++executed_tool_calls; |
| } |
| if (executed_tool_calls == 0) { |
| *analysis = "AI analysis could not execute requested tool calls."; |
| return Status::OK(); |
| } |
| continue; |
| } |
| |
| string assistant_content; |
| if (assistant_message != nullptr |
| && TryExtractAssistantContentText(assistant_message, &assistant_content)) { |
| conversation.emplace_back(ROLE_ASSISTANT, assistant_content, "", ""); |
| *analysis = move(assistant_content); |
| return Status::OK(); |
| } |
| } |
| *analysis = last_response; |
| return Status::OK(); |
| } |
| |
| Document response_doc; |
| if (ParseJsonObjectFromResponse(last_response, &response_doc)) { |
| const Value* assistant_message = FindAssistantMessageInResponse(response_doc); |
| if (assistant_message != nullptr && assistant_message->HasMember(JSON_KEY_TOOL_CALLS) |
| && (*assistant_message)[JSON_KEY_TOOL_CALLS].IsArray() |
| && !(*assistant_message)[JSON_KEY_TOOL_CALLS].Empty()) { |
| *analysis = "AI analysis did not complete within the tool-calling iteration " |
| "limit. Please retry."; |
| return Status::OK(); |
| } |
| } |
| *analysis = last_response.empty() ? "AI analysis did not complete." |
| : move(last_response); |
| return Status::OK(); |
| } |
| |
| // Redacts profile input, runs AI analysis with profile tools, then unredacts output. |
| Status GenerateAiAnalysisFromProfile(const Value& profile_json, string* analysis) { |
| if (analysis == nullptr) return Status("analysis output pointer cannot be null"); |
| if (FLAGS_query_profile_ai_analysis_max_concurrent_runs == 0) { |
| return Status("Query profile AI analysis is disabled."); |
| } |
| if (!profile_json.IsObject()) { |
| return Status("Query profile input must be a valid JSON object"); |
| } |
| if (!TryAcquireAiAnalysisSlot()) { |
| return Status(strings::Substitute( |
| "Too many concurrent query-profile AI analysis runs. Limit: $0. " |
| "Please retry.", |
| FLAGS_query_profile_ai_analysis_max_concurrent_runs)); |
| } |
| const auto slot_release = MakeScopeExitTrigger([]() { ReleaseAiAnalysisSlot(); }); |
| |
| int64_t profile_size_limit_bytes = 0; |
| RETURN_IF_ERROR(ComputeProfileAnalysisSizeLimitBytes(&profile_size_limit_bytes)); |
| |
| QueryProfileRedactor redactor(profile_size_limit_bytes); |
| RETURN_IF_ERROR(redactor.Redact(profile_json)); |
| const size_t redacted_profile_size_bytes = |
| JsonToString(redactor.redacted_profile_json()).size(); |
| |
| QueryProfileToolExecutor profile_tool_executor; |
| RETURN_IF_ERROR(CreateQueryProfileToolExecutorForProfile( |
| redactor.redacted_profile_json(), &profile_tool_executor, |
| profile_size_limit_bytes, redacted_profile_size_bytes)); |
| |
| Document summary_tool_output_doc; |
| RETURN_IF_ERROR(profile_tool_executor("get_summary", "", &summary_tool_output_doc)); |
| Document initial_summary_doc; |
| initial_summary_doc.SetObject(); |
| initial_summary_doc.AddMember( |
| JSON_KEY_SUMMARY, |
| Value(summary_tool_output_doc, initial_summary_doc.GetAllocator()), |
| initial_summary_doc.GetAllocator()); |
| const string initial_summary_json = JsonToString(initial_summary_doc); |
| |
| RETURN_IF_ERROR(RunAiAgentWithToolExecutor(initial_summary_json, |
| [&profile_tool_executor]( |
| string_view tool_name, string_view tool_args_json, string* tool_json) { |
| if (tool_json == nullptr) { |
| return Status("tool output pointer cannot be null"); |
| } |
| Document tool_result_doc; |
| RETURN_IF_ERROR( |
| profile_tool_executor(tool_name, tool_args_json, &tool_result_doc)); |
| Document tool_doc; |
| tool_doc.SetObject(); |
| tool_doc.AddMember(JSON_KEY_TOOL_NAME, |
| MakeJsonStringValue(tool_name, tool_doc.GetAllocator()), |
| tool_doc.GetAllocator()); |
| tool_doc.AddMember(JSON_KEY_RESULT, |
| Value(tool_result_doc, tool_doc.GetAllocator()), tool_doc.GetAllocator()); |
| *tool_json = JsonToString(tool_doc); |
| return Status::OK(); |
| }, analysis)); |
| *analysis = redactor.Unredact(*analysis); |
| return Status::OK(); |
| } |
| |
| } // namespace impala |