1515
1616import copy
1717import datetime
18+ import hashlib
1819import json
1920import logging
2021import os
2526from absl import flags
2627from perfkitbenchmarker import data
2728from perfkitbenchmarker import edw_service
29+ from perfkitbenchmarker import errors
2830from perfkitbenchmarker import provider_info
2931from perfkitbenchmarker import vm_util
3032from perfkitbenchmarker .linux_packages import google_cloud_sdk
4042 ' benchmarking.' ,
4143)
4244
45+ BQ_CA_AGENT = flags .DEFINE_string (
46+ 'bq_ca_agent' ,
47+ None ,
48+ 'The full resource name of the agent to use, e.g. '
49+ 'projects/{project}/locations/{location}/dataAgents/{agent_id}' ,
50+ )
51+
4352BQ_CLIENT_FILE = 'bq-jdbc-simba-client-1.8-temp-labels.jar'
4453BQ_PYTHON_CLIENT_FILE = 'bq_python_driver.py'
4554BQ_PYTHON_CLIENT_DIR = 'edw/bigquery/clients/python'
@@ -508,6 +517,116 @@ def RunQueryWithResults(self, query_name: str) -> str:
508517 return stdout
509518
510519
520+ class ConversationalAnalyticsClientInterface (PythonClientInterface ):
521+ """Conversational Analytics Client Interface subclassing PythonClientInterface."""
522+
523+ def _GetQueryFileName (self , query_name : str ) -> str :
524+ """Generates a filename from a query name."""
525+ sanitized = re .sub (r'[^a-zA-Z0-9_-]' , '_' , query_name )
526+ # Limit sanitized part to 30 chars
527+ sanitized = sanitized [:30 ]
528+ # Add hash of full query name to ensure uniqueness
529+ query_hash = hashlib .md5 (query_name .encode ('utf-8' )).hexdigest ()[:8 ]
530+ return f'./{ sanitized } _{ query_hash } .txt'
531+
532+ def _ParseConversationalAnalyticsResults (
533+ self , results : dict [str , Any ], query_name : str
534+ ) -> tuple [float , dict [str , Any ]]:
535+ """Parses the results from Conversational Analytics query execution."""
536+ execution_time = results .get ('query_wall_time_in_secs' , - 1.0 )
537+ details = results .get ('details' , {})
538+ query_results = details .get ('query_results' , {})
539+
540+ # Extract essential fields
541+ text_response = query_results .get ('text_response' )
542+ generated_sql = query_results .get ('generated_sql' )
543+ retrieved_data = query_results .get ('retrieved_data' )
544+ # Fail fast validation
545+ error_msg = None
546+ if not text_response :
547+ error_msg = f"'text_response' is missing or empty. Got: { text_response !r} "
548+ elif not generated_sql :
549+ error_msg = f"'generated_sql' is missing or empty. Got: { generated_sql !r} "
550+ elif not retrieved_data :
551+ error_msg = (
552+ f"'retrieved_data' is missing or empty. Got: { retrieved_data !r} "
553+ )
554+
555+ metadata = {
556+ 'question' : query_name ,
557+ 'text_response' : text_response or '' ,
558+ 'generated_sql' : generated_sql or '' ,
559+ 'retrieved_data' : retrieved_data or [],
560+ 'thoughts' : query_results .get ('thoughts' , []),
561+ 'progress_messages' : query_results .get ('progress_messages' , []),
562+ 'time_to_first_token_secs' : query_results .get (
563+ 'time_to_first_token_secs' , 0.0
564+ ),
565+ 'total_stream_time_secs' : query_results .get (
566+ 'total_stream_time_secs' , 0.0
567+ ),
568+ 'job_id' : details .get ('job_id' , '' ),
569+ }
570+
571+ if error_msg :
572+ logging .warning ('Conversational Analytics query failed: %s' , error_msg )
573+ metadata ['error' ] = f'Conversational Analytics query failed: { error_msg } '
574+ return - 1.0 , metadata
575+
576+ logging .info (
577+ 'Conversational Analytics Response: %s' ,
578+ metadata .get ('text_response' , '' ),
579+ )
580+ return execution_time , metadata
581+
582+ @override
583+ def Prepare (self , package_name : str ) -> None :
584+ """Prepares the client vm by installing geminidataanalytics package and driver."""
585+ super ().Prepare (package_name )
586+
587+ # Install BQ CA specific SDK
588+ self .client_vm .RemoteCommand (
589+ 'source .venv/bin/activate && pip install'
590+ ' google-cloud-geminidataanalytics'
591+ )
592+
593+ # Push Conversational Analytics driver script.
594+ driver_local_path = data .ResourcePath (
595+ os .path .join ('edw/bigquery/clients/python' , 'bq_ca_driver.py' )
596+ )
597+ self .client_vm .PushFile (driver_local_path , 'bq_ca_driver.py' )
598+
599+ @override
600+ def ExecuteQuery (
601+ self , query_name : str , print_results : bool = False
602+ ) -> tuple [float , dict [str , Any ]]:
603+ """Executes a single conversational analytics question."""
604+ logging .info ('Conversational Analytics Question: %s' , query_name )
605+ key_file = os .path .basename (FLAGS .gcp_service_account_key_file )
606+ cmd = (
607+ 'source .venv/bin/activate && python3 bq_ca_driver.py single '
608+ f'--project={ self .project_id } '
609+ f'--agent={ BQ_CA_AGENT .value } '
610+ f'--credentials_file={ key_file } '
611+ '--print_results '
612+ )
613+
614+ remote_query_file = self ._GetQueryFileName (query_name )
615+ # Write question to remote file to handle quotes securely
616+ vm_util .CreateRemoteFile (self .client_vm , query_name , remote_query_file )
617+ cmd += f'--query_file={ remote_query_file } '
618+
619+ stdout , _ = self .client_vm .RemoteCommand (cmd )
620+
621+ try :
622+ results = json .loads (stdout )
623+ except ValueError as e :
624+ raise errors .Benchmarks .RunError (
625+ f'Failed to parse Conversational Analytics response: { stdout } '
626+ ) from e
627+ return self ._ParseConversationalAnalyticsResults (results , query_name )
628+
629+
511630class Bigquery (edw_service .EdwService ):
512631 """Object representing a Bigquery cluster.
513632
@@ -526,6 +645,14 @@ def __init__(self, edw_service_spec):
526645 project_id , dataset_id = _SplitClusterIdentifier (self .cluster_identifier )
527646 self .client_interface = GetBigQueryClientInterface (project_id , dataset_id )
528647
648+ @override
649+ def GetConversationalAnalyticsClientInterface (
650+ self ,
651+ ) -> ConversationalAnalyticsClientInterface :
652+ """Returns the Conversational Analytics Client Interface instance."""
653+ project_id , dataset_id = _SplitClusterIdentifier (self .cluster_identifier )
654+ return ConversationalAnalyticsClientInterface (project_id , dataset_id )
655+
529656 def _Create (self ):
530657 """Create a BigQuery cluster.
531658
0 commit comments