diff --git a/google/generativeai/types/content_types.py b/google/generativeai/types/content_types.py index 7e343a5c0..74f03029c 100644 --- a/google/generativeai/types/content_types.py +++ b/google/generativeai/types/content_types.py @@ -623,24 +623,40 @@ def _encode_fd(fd: FunctionDeclaration | protos.FunctionDeclaration) -> protos.F class Tool: """A wrapper for `protos.Tool`, Contains a collection of related `FunctionDeclaration` objects.""" - def __init__(self, function_declarations: Iterable[FunctionDeclarationType]): + def __init__( + self, + function_declarations: Iterable[FunctionDeclarationType] | None = None, + code_execution: protos.CodeExecution | None = None, + ): # The main path doesn't use this but is seems useful. - self._function_declarations = [_make_function_declaration(f) for f in function_declarations] - self._index = {} - for fd in self._function_declarations: - name = fd.name - if name in self._index: - raise ValueError("") - self._index[fd.name] = fd + if function_declarations: + self._function_declarations = [ + _make_function_declaration(f) for f in function_declarations + ] + self._index = {} + for fd in self._function_declarations: + name = fd.name + if name in self._index: + raise ValueError("") + self._index[fd.name] = fd + else: + # Consistent fields + self._function_declarations = [] + self._index = {} self._proto = protos.Tool( - function_declarations=[_encode_fd(fd) for fd in self._function_declarations] + function_declarations=[_encode_fd(fd) for fd in self._function_declarations], + code_execution=code_execution, ) @property def function_declarations(self) -> list[FunctionDeclaration | protos.FunctionDeclaration]: return self._function_declarations + @property + def code_execution(self) -> protos.CodeExecution: + return self._proto.code_execution + def __getitem__( self, name: str | protos.FunctionCall ) -> FunctionDeclaration | protos.FunctionDeclaration: @@ -673,13 +689,24 @@ def _make_tool(tool: ToolType) -> Tool: if isinstance(tool, Tool): return tool elif isinstance(tool, protos.Tool): - return Tool(function_declarations=tool.function_declarations) + if "code_execution" in tool: + code_execution = tool.code_execution + else: + code_execution = None + return Tool(function_declarations=tool.function_declarations, code_execution=code_execution) elif isinstance(tool, dict): - if "function_declarations" in tool: + if "function_declarations" in tool or "code_execution" in tool: return Tool(**tool) else: fd = tool return Tool(function_declarations=[protos.FunctionDeclaration(**fd)]) + elif isinstance(tool, str): + if tool.lower() == "code_execution": + return Tool(code_execution=protos.CodeExecution()) + else: + raise ValueError("The only string that can be passed as a tool is 'code_execution'.") + elif isinstance(tool, protos.CodeExecution): + return Tool(code_execution=tool) elif isinstance(tool, Iterable): return Tool(function_declarations=tool) else: @@ -734,7 +761,12 @@ def to_proto(self): def _make_tools(tools: ToolsType) -> list[Tool]: - if isinstance(tools, Iterable) and not isinstance(tools, Mapping): + if isinstance(tools, str): + if tools.lower() == "code_execution": + return [_make_tool(tools)] + else: + raise ValueError("The only string that can be passed as a tool is 'code_execution'.") + elif isinstance(tools, Iterable) and not isinstance(tools, Mapping): tools = [_make_tool(t) for t in tools] if len(tools) > 1 and all(len(t.function_declarations) == 1 for t in tools): # flatten into a single tool. diff --git a/google/generativeai/types/file_types.py b/google/generativeai/types/file_types.py index ef251e296..f77d373e3 100644 --- a/google/generativeai/types/file_types.py +++ b/google/generativeai/types/file_types.py @@ -15,7 +15,7 @@ from __future__ import annotations import datetime -from typing import Union +from typing import Any, Union from typing_extensions import TypedDict from google.rpc.status_pb2 import Status @@ -23,6 +23,8 @@ from google.generativeai import protos +import pprint + class File: def __init__(self, proto: protos.File | File | dict): @@ -33,6 +35,27 @@ def __init__(self, proto: protos.File | File | dict): def to_proto(self) -> protos.File: return self._proto + def to_dict(self) -> dict[str, Any]: + return type(self._proto).to_dict(self._proto, use_integers_for_enums=False) + + def __str__(self): + def sort_key(pair): + name, value = pair + if name == "name": + return "" + elif "time" in name: + return "zz_" + name + else: + return name + + dict_format = dict(sorted(self.to_dict().items(), key=sort_key)) + dict_format = pprint.pformat(dict_format, sort_dicts=False) + dict_format = "{\n " + dict_format[1:] + dict_format = "\n ".join(dict_format.splitlines()) + return dict_format.join(["genai.File(", ")"]) + + __repr__ = __str__ + @property def name(self) -> str: return self._proto.name diff --git a/google/generativeai/types/generation_types.py b/google/generativeai/types/generation_types.py index 20686a156..d4bed8b86 100644 --- a/google/generativeai/types/generation_types.py +++ b/google/generativeai/types/generation_types.py @@ -261,19 +261,33 @@ def _join_contents(contents: Iterable[protos.Content]): for content in contents: parts.extend(content.parts) - merged_parts = [parts.pop(0)] - for part in parts: - if not merged_parts[-1].text: - merged_parts.append(part) + merged_parts = [] + last = parts[0] + for part in parts[1:]: + if "text" in last and "text" in part: + last = protos.Part(text=last.text + part.text) continue - if not part.text: - merged_parts.append(part) + # Can we merge the new thing into last? + # If not, put last in list of parts, and new thing becomes last + if "executable_code" in last and "executable_code" in part: + last = protos.Part( + executable_code=_join_executable_code(last.executable_code, part.executable_code) + ) continue - merged_part = protos.Part(merged_parts[-1]) - merged_part.text += part.text - merged_parts[-1] = merged_part + if "code_execution_result" in last and "code_execution_result" in part: + last = protos.Part( + code_execution_result=_join_code_execution_result( + last.code_execution_result, part.code_execution_result + ) + ) + continue + + merged_parts.append(last) + last = part + + merged_parts.append(last) return protos.Content( role=role, @@ -281,6 +295,16 @@ def _join_contents(contents: Iterable[protos.Content]): ) +def _join_executable_code(code_1, code_2): + return protos.ExecutableCode(language=code_1.language, code=code_1.code + code_2.code) + + +def _join_code_execution_result(result_1, result_2): + return protos.CodeExecutionResult( + outcome=result_2.outcome, output=result_1.output + result_2.output + ) + + def _join_candidates(candidates: Iterable[protos.Candidate]): candidates = tuple(candidates) @@ -413,13 +437,35 @@ def text(self): "Invalid operation: The `response.text` quick accessor requires the response to contain a valid `Part`, " "but none were returned. Please check the `candidate.safety_ratings` to determine if the response was blocked." ) - if len(parts) != 1 or "text" not in parts[0]: - raise ValueError( - "Invalid operation: The `response.text` quick accessor requires a simple (single-`Part`) text response. " - "This response is not simple text. Please use the `result.parts` accessor or the full " - "`result.candidates[index].content.parts` lookup instead." - ) - return parts[0].text + + texts = [] + for part in parts: + if "text" in part: + texts.append(part.text) + continue + if "executable_code" in part: + language = part.executable_code.language.name.lower() + if language == "language_unspecified": + language = "" + else: + language = f" {language}" + texts.extend([f"```{language}", part.executable_code.code.lstrip("\n"), "```"]) + continue + if "code_execution_result" in part: + outcome_result = part.code_execution_result.outcome.name.lower().replace( + "outcome_", "" + ) + if outcome_result == "ok" or outcome_result == "unspecified": + outcome_result = "" + else: + outcome_result = f" {outcome_result}" + texts.extend([f"```{outcome_result}", part.code_execution_result.output, "```"]) + continue + + part_type = protos.Part.pb(part).whichOneof("data") + raise ValueError(f"Could not convert `part.{part_type}` to text.") + + return "\n".join(texts) @property def prompt_feedback(self): diff --git a/google/generativeai/version.py b/google/generativeai/version.py index 69a8b817e..20f814c4b 100644 --- a/google/generativeai/version.py +++ b/google/generativeai/version.py @@ -14,4 +14,4 @@ # limitations under the License. from __future__ import annotations -__version__ = "0.7.0" +__version__ = "0.7.1" diff --git a/samples/embed.py b/samples/embed.py new file mode 100644 index 000000000..460393dec --- /dev/null +++ b/samples/embed.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +# Copyright 2023 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. +from absl.testing import absltest + + +import google.generativeai as genai + + +class UnitTests(absltest.TestCase): + def test_embed_content(self): + # [START embed_content] + + text = "Hello World!" + result = genai.embed_content( + model="models/text-embedding-004", content=text, output_dimensionality=10 + ) + print(result["embedding"]) + print() + # [END embed_content] + + def batch_embed_content(self): + # [START batch_embed_content] + texts = [ + "What is the meaning of life?", + "How much wood would a woodchuck chuck?", + "How does the brain work?", + ] + result = genai.embed_content( + model="models/text-embedding-004", content=texts, output_dimensionality=10 + ) + print(result) + print() + # [END batch_embed_content] + + +if __name__ == "__main__": + absltest.main() diff --git a/samples/files.py b/samples/files.py new file mode 100644 index 000000000..8a9fde614 --- /dev/null +++ b/samples/files.py @@ -0,0 +1,93 @@ +# -*- coding: utf-8 -*- +# Copyright 2023 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. +from absl.testing import absltest + +import google +import google.generativeai as genai +import pathlib + +media = pathlib.Path(__file__).parents[1] / "third_party" + + +class UnitTests(absltest.TestCase): + def test_files_create(self): + # [START files_create] + myfile = genai.upload_file(media / "poem.txt") + print(f"{myfile=}") + print() + + model = genai.GenerativeModel("gemini-1.5-flash") + result = model.generate_content( + [myfile, "\n\n", "Can you add a few more lines to this poem?"] + ) + print(f"{result.text=}") + print() + # [END files_create] + + def test_files_create_video(self): + # [START files_create_video] + import time + + # Video clip from https://peach.blender.org/download/ + myfile = genai.upload_file(media / "Big_Buck_Bunny.mp4") + print(f"{myfile=}") + print() + + while myfile.state.name == "PROCESSING": + print("processing video...") + time.sleep(5) + myfile = genai.get_file(myfile.name) + + model = genai.GenerativeModel("gemini-1.5-flash") + result = model.generate_content([myfile, "Describe this video clip"]) + print() + print(f"{result.text=}") + print() + # [END files_create_video] + + def test_files_list(self): + # [START files_list] + print("My files:") + for f in genai.list_files(): + print(" ", f.name) + # [END files_list] + + def test_files_delete(self): + # [START files_delete] + myfile = genai.upload_file(media / "poem.txt") + + myfile.delete() + + try: + # Error. + model = genai.GenerativeModel("gemini-1.5-flash") + result = model.generate_content([myfile, "Describe this file."]) + except google.api_core.exceptions.PermissionDenied: + pass + # [END files_delete] + + def test_files_get(self): + # [START files_get] + myfile = genai.upload_file(media / "poem.txt") + file_name = myfile.name + print(file_name) # "files/*" + + myfile = genai.get_file(file_name) + print(myfile) + # [END files_get] + + +if __name__ == "__main__": + absltest.main() diff --git a/samples/json_mode.py b/samples/json_mode.py new file mode 100644 index 000000000..c29aa30e2 --- /dev/null +++ b/samples/json_mode.py @@ -0,0 +1,38 @@ +# +# 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. +from absl.testing import absltest + +import google.generativeai as genai +import typing_extensions as typing + + +class UnitTests(absltest.TestCase): + def test_controlled_generation(self): + # [START controlled_generation] + class Recipe(typing.TypedDict): + recipe_name: str + + model = genai.GenerativeModel("gemini-1.5-pro-latest") + result = model.generate_content( + "List a few popular cookie recipes.", + generation_config=genai.GenerationConfig( + response_mime_type="application/json", response_schema=list([Recipe]) + ), + ) + print(result) + print() + # [END controlled_generation] + + +if __name__ == "__main__": + absltest.main() diff --git a/setup.py b/setup.py index 89af61515..b4b05e619 100644 --- a/setup.py +++ b/setup.py @@ -42,7 +42,7 @@ def get_version(): release_status = "Development Status :: 5 - Production/Stable" dependencies = [ - "google-ai-generativelanguage==0.6.5", + "google-ai-generativelanguage==0.6.6", "google-api-core", "google-api-python-client", "google-auth>=2.15.0", # 2.15 adds API key auth support diff --git a/tests/test_content.py b/tests/test_content.py index 6df5faad4..5b7aa9781 100644 --- a/tests/test_content.py +++ b/tests/test_content.py @@ -15,7 +15,7 @@ import dataclasses import pathlib import typing_extensions -from typing import Any, Union +from typing import Any, Union, Iterable from absl.testing import absltest from absl.testing import parameterized @@ -367,7 +367,7 @@ def test_to_tools(self, tools): raise ValueError("This shouldn't happen") tools = function_library.to_proto() - tools = type(tools[0]).to_dict(tools[0]) + tools = type(tools[0]).to_dict(tools[0], including_default_value_fields=False) tools["function_declarations"][0].pop("parameters", None) expected = dict( @@ -378,6 +378,24 @@ def test_to_tools(self, tools): self.assertEqual(tools, expected) + @parameterized.named_parameters( + ["string", "code_execution"], + ["proto_object", protos.CodeExecution()], + ["proto_passed_in", protos.Tool(code_execution=protos.CodeExecution())], + ["empty_dictionary", {"code_execution": {}}], + ["string_list", ["code_execution"]], + ["proto_object_list", [protos.CodeExecution()]], + ["proto_passed_in_list", [protos.Tool(code_execution=protos.CodeExecution())]], + ["empty_dictionary_list", [{"code_execution": {}}]], + ) + def test_code_execution(self, tools): + if isinstance(tools, Iterable): + t = content_types._make_tools(tools) + self.assertIsInstance(t[0].code_execution, protos.CodeExecution) + else: + t = content_types._make_tool(tools) # Pass code execution into tools + self.assertIsInstance(t.code_execution, protos.CodeExecution) + def test_two_fun_is_one_tool(self): def a(): pass diff --git a/tests/test_generation.py b/tests/test_generation.py index 828577d21..3a5363d72 100644 --- a/tests/test_generation.py +++ b/tests/test_generation.py @@ -124,6 +124,61 @@ def test_join_contents(self): self.assertEqual(expected, type(result).to_dict(result)) + def test_join_parts(self): + contents = [ + protos.Content(role="assistant", parts=[protos.Part(text="A")]), + protos.Content(role="assistant", parts=[protos.Part(text="B")]), + protos.Content(role="assistant", parts=[protos.Part(executable_code={"code": "C"})]), + protos.Content(role="assistant", parts=[protos.Part(executable_code={"code": "D"})]), + protos.Content( + role="assistant", parts=[protos.Part(code_execution_result={"output": "E"})] + ), + protos.Content( + role="assistant", parts=[protos.Part(code_execution_result={"output": "F"})] + ), + protos.Content(role="assistant", parts=[protos.Part(text="G")]), + protos.Content(role="assistant", parts=[protos.Part(text="H")]), + ] + g = generation_types._join_contents(contents=contents) + expected = protos.Content( + role="assistant", + parts=[ + protos.Part(text="AB"), + protos.Part(executable_code={"code": "CD"}), + protos.Part(code_execution_result={"output": "EF"}), + protos.Part(text="GH"), + ], + ) + self.assertEqual(expected, g) + + def test_code_execution_text(self): + content = protos.Content( + role="assistant", + parts=[ + protos.Part(text="AB"), + protos.Part(executable_code={"language": "PYTHON", "code": "CD"}), + protos.Part(code_execution_result={"outcome": "OUTCOME_OK", "output": "EF"}), + protos.Part(text="GH"), + ], + ) + response = generation_types.GenerateContentResponse( + done=True, + iterator=None, + result=protos.GenerateContentResponse({"candidates": [{"content": content}]}), + ) + expected = textwrap.dedent( + """\ + AB + ``` python + CD + ``` + ``` + EF + ``` + GH""" + ) + self.assertEqual(expected, response.text) + def test_many_join_contents(self): import string diff --git a/third_party/Big_Buck_Bunny.mp4 b/third_party/Big_Buck_Bunny.mp4 new file mode 100644 index 000000000..07096db47 Binary files /dev/null and b/third_party/Big_Buck_Bunny.mp4 differ diff --git a/third_party/LICENSE.txt b/third_party/LICENSE.txt new file mode 100644 index 000000000..33ae178a7 --- /dev/null +++ b/third_party/LICENSE.txt @@ -0,0 +1,6 @@ +* poem.txt: + * This is the first paragraph from shakespear's spring, no copyright. +* Big_Buck_Bunny.mp4: + * This is a clip from https://peach.blender.org/download/ + * License CC-BY 3.0 (Attribution) + diff --git a/third_party/poem.txt b/third_party/poem.txt new file mode 100644 index 000000000..35845dc6e --- /dev/null +++ b/third_party/poem.txt @@ -0,0 +1,4 @@ +When daisies pied, and violets blue, +And lady-smocks all silver-white, +And cuckoo-buds of yellow hue +Do paint the meadows with delight