mirror of
https://github.com/EvolutionAPI/adk-python.git
synced 2025-07-13 07:04:51 -06:00
246 lines
6.7 KiB
Python
246 lines
6.7 KiB
Python
# Copyright 2025 Google LLC
|
|
#
|
|
# Licensed 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.
|
|
|
|
"""Utility functions for code execution."""
|
|
|
|
import base64
|
|
import binascii
|
|
import copy
|
|
import dataclasses
|
|
import re
|
|
from typing import List, Optional
|
|
|
|
from google.genai import types
|
|
|
|
|
|
@dataclasses.dataclass(frozen=True)
|
|
class File:
|
|
"""A structure that contains a file name and its content."""
|
|
|
|
name: str
|
|
"""
|
|
The name of the file with file extension (e.g., "file.csv").
|
|
"""
|
|
|
|
content: str
|
|
"""
|
|
The base64-encoded bytes of the file content.
|
|
"""
|
|
|
|
mime_type: str = 'text/plain'
|
|
"""
|
|
The mime type of the file (e.g., "image/png").
|
|
"""
|
|
|
|
|
|
@dataclasses.dataclass
|
|
class CodeExecutionInput:
|
|
"""A structure that contains the input of code execution."""
|
|
|
|
code: str
|
|
"""
|
|
The code to execute.
|
|
"""
|
|
|
|
input_files: list[File] = dataclasses.field(default_factory=list)
|
|
"""
|
|
The input files available to the code.
|
|
"""
|
|
|
|
execution_id: Optional[str] = None
|
|
"""
|
|
The execution ID for the stateful code execution.
|
|
"""
|
|
|
|
|
|
@dataclasses.dataclass
|
|
class CodeExecutionResult:
|
|
"""A structure that contains the result of code execution."""
|
|
|
|
stdout: str = ''
|
|
"""
|
|
The standard output of the code execution.
|
|
"""
|
|
|
|
stderr: str = ''
|
|
"""
|
|
The standard error of the code execution.
|
|
"""
|
|
|
|
output_files: list[File] = dataclasses.field(default_factory=list)
|
|
"""
|
|
The output files from the code execution.
|
|
"""
|
|
|
|
|
|
class CodeExecutionUtils:
|
|
"""Utility functions for code execution."""
|
|
|
|
@staticmethod
|
|
def get_encoded_file_content(data: bytes) -> bytes:
|
|
"""Gets the file content as a base64-encoded bytes.
|
|
|
|
Args:
|
|
data: The file content bytes.
|
|
|
|
Returns:
|
|
The file content as a base64-encoded bytes.
|
|
"""
|
|
|
|
def _is_base64_encoded(data: bytes) -> bool:
|
|
try:
|
|
return base64.b64encode(base64.b64decode(data)) == data
|
|
except binascii.Error:
|
|
return False
|
|
|
|
return data if _is_base64_encoded(data) else base64.b64encode(data)
|
|
|
|
@staticmethod
|
|
def extract_code_and_truncate_content(
|
|
content: types.Content,
|
|
code_block_delimiters: List[tuple[str, str]],
|
|
) -> Optional[str]:
|
|
"""Extracts the first code block from the content and truncate everything after it.
|
|
|
|
Args:
|
|
content: The mutable content to extract the code from.
|
|
code_block_delimiters: The list of the enclosing delimiters to identify
|
|
the code blocks.
|
|
|
|
Returns:
|
|
The first code block if found, otherwise None.
|
|
"""
|
|
if not content or not content.parts:
|
|
return
|
|
|
|
text_parts = [p for p in content.parts if p.text]
|
|
if not text_parts:
|
|
return
|
|
|
|
first_text_part = copy.deepcopy(text_parts[0])
|
|
response_text = '\n'.join([p.text for p in text_parts])
|
|
|
|
# Find the first code block.
|
|
leading_delimiter_pattern = '|'.join(d[0] for d in code_block_delimiters)
|
|
trailing_delimiter_pattern = '|'.join(d[1] for d in code_block_delimiters)
|
|
pattern = re.compile(
|
|
(
|
|
rf'(?P<prefix>.*?)({leading_delimiter_pattern})(?P<code>.*?)({trailing_delimiter_pattern})(?P<suffix>.*?)$'
|
|
).encode(),
|
|
re.DOTALL,
|
|
)
|
|
pattern_match = pattern.search(response_text.encode())
|
|
if pattern_match is None:
|
|
return
|
|
|
|
code_str = pattern_match.group('code').decode()
|
|
if not code_str:
|
|
return
|
|
|
|
content.parts = []
|
|
if pattern_match.group('prefix'):
|
|
first_text_part.text = pattern_match.group('prefix').decode()
|
|
content.parts.append(first_text_part)
|
|
content.parts.append(
|
|
CodeExecutionUtils.build_executable_code_part(code_str)
|
|
)
|
|
return pattern_match.group('code').decode()
|
|
|
|
@staticmethod
|
|
def build_executable_code_part(code: str) -> types.Part:
|
|
"""Builds an executable code part with code string.
|
|
|
|
Args:
|
|
code: The code string.
|
|
|
|
Returns:
|
|
The constructed executable code part.
|
|
"""
|
|
return types.Part.from_executable_code(
|
|
code=code,
|
|
language='PYTHON',
|
|
)
|
|
|
|
@staticmethod
|
|
def build_code_execution_result_part(
|
|
code_execution_result: CodeExecutionResult,
|
|
) -> types.Part:
|
|
"""Builds the code execution result part from the code execution result.
|
|
|
|
Args:
|
|
code_execution_result: The code execution result.
|
|
|
|
Returns:
|
|
The constructed code execution result part.
|
|
"""
|
|
if code_execution_result.stderr:
|
|
return types.Part.from_code_execution_result(
|
|
outcome='OUTCOME_FAILED',
|
|
output=code_execution_result.stderr,
|
|
)
|
|
final_result = []
|
|
if code_execution_result.stdout or not code_execution_result.output_files:
|
|
final_result.append(
|
|
'Code execution result:\n' + '%s\n' % code_execution_result.stdout
|
|
)
|
|
if code_execution_result.output_files:
|
|
final_result.append(
|
|
'Saved artifacts:\n'
|
|
+ ','.join(
|
|
['`%s`' % f.name for f in code_execution_result.output_files]
|
|
)
|
|
)
|
|
return types.Part.from_code_execution_result(
|
|
outcome='OUTCOME_OK',
|
|
output='\n\n'.join(final_result),
|
|
)
|
|
|
|
@staticmethod
|
|
def convert_code_execution_parts(
|
|
content: types.Content,
|
|
code_block_delimiter: tuple[str, str],
|
|
execution_result_delimiters: tuple[str, str],
|
|
):
|
|
"""Converts the code execution parts to text parts in a Content.
|
|
|
|
Args:
|
|
content: The mutable content to convert the code execution parts to text
|
|
parts.
|
|
code_block_delimiter: The delimiter to format the code block.
|
|
execution_result_delimiters: The delimiter to format the code execution
|
|
result.
|
|
"""
|
|
if not content.parts:
|
|
return
|
|
|
|
# Handle the conversion of trailing executable code parts.
|
|
if content.parts[-1].executable_code:
|
|
content.parts[-1] = types.Part(
|
|
text=(
|
|
code_block_delimiter[0]
|
|
+ content.parts[-1].executable_code.code
|
|
+ code_block_delimiter[1]
|
|
)
|
|
)
|
|
# Handle the conversion of trailing code execution result parts.
|
|
# Skip if the Content has multiple parts, which means the Content is
|
|
# likely generated by the model.
|
|
elif len(content.parts) == 1 and content.parts[-1].code_execution_result:
|
|
content.parts[-1] = types.Part(
|
|
text=execution_result_delimiters[0]
|
|
+ content.parts[-1].code_execution_result.output
|
|
+ execution_result_delimiters[1]
|
|
)
|
|
content.role = 'user'
|