From fb4ac30042967c403344aeec5484faee1ef6e9a0 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Fri, 12 Jan 2024 09:59:35 -0500 Subject: [PATCH 1/8] Add vision --- docs/ai/{ => audio}/speech.md | 0 docs/ai/{ => images}/painting.md | 0 docs/ai/{ => text}/casting.md | 0 docs/ai/{ => text}/classification.md | 0 docs/ai/{ => text}/extraction.md | 0 docs/ai/{ => text}/function.md | 0 docs/ai/{ => text}/generation.md | 0 docs/ai/vision/captioning.md | 38 +++++++ docs/api_reference/ai/beta/vision.md | 6 ++ docs/api_reference/requests.md | 1 - docs/api_reference/types.md | 1 + docs/assets/images/core/vision/marvin.webp | Bin 0 -> 53182 bytes mkdocs.yml | 23 +++-- src/marvin/__init__.py | 3 + src/marvin/ai/beta/__init__.py | 0 src/marvin/ai/beta/vision.py | 115 +++++++++++++++++++++ src/marvin/ai/prompts/vision_prompts.py | 15 +++ src/marvin/ai/text.py | 5 +- src/marvin/client/openai.py | 42 +++++++- src/marvin/settings.py | 16 +++ src/marvin/types.py | 52 +++++++++- src/marvin/utilities/images.py | 15 +++ 22 files changed, 312 insertions(+), 20 deletions(-) rename docs/ai/{ => audio}/speech.md (100%) rename docs/ai/{ => images}/painting.md (100%) rename docs/ai/{ => text}/casting.md (100%) rename docs/ai/{ => text}/classification.md (100%) rename docs/ai/{ => text}/extraction.md (100%) rename docs/ai/{ => text}/function.md (100%) rename docs/ai/{ => text}/generation.md (100%) create mode 100644 docs/ai/vision/captioning.md create mode 100644 docs/api_reference/ai/beta/vision.md delete mode 100644 docs/api_reference/requests.md create mode 100644 docs/api_reference/types.md create mode 100644 docs/assets/images/core/vision/marvin.webp create mode 100644 src/marvin/ai/beta/__init__.py create mode 100644 src/marvin/ai/beta/vision.py create mode 100644 src/marvin/ai/prompts/vision_prompts.py create mode 100644 src/marvin/utilities/images.py diff --git a/docs/ai/speech.md b/docs/ai/audio/speech.md similarity index 100% rename from docs/ai/speech.md rename to docs/ai/audio/speech.md diff --git a/docs/ai/painting.md b/docs/ai/images/painting.md similarity index 100% rename from docs/ai/painting.md rename to docs/ai/images/painting.md diff --git a/docs/ai/casting.md b/docs/ai/text/casting.md similarity index 100% rename from docs/ai/casting.md rename to docs/ai/text/casting.md diff --git a/docs/ai/classification.md b/docs/ai/text/classification.md similarity index 100% rename from docs/ai/classification.md rename to docs/ai/text/classification.md diff --git a/docs/ai/extraction.md b/docs/ai/text/extraction.md similarity index 100% rename from docs/ai/extraction.md rename to docs/ai/text/extraction.md diff --git a/docs/ai/function.md b/docs/ai/text/function.md similarity index 100% rename from docs/ai/function.md rename to docs/ai/text/function.md diff --git a/docs/ai/generation.md b/docs/ai/text/generation.md similarity index 100% rename from docs/ai/generation.md rename to docs/ai/text/generation.md diff --git a/docs/ai/vision/captioning.md b/docs/ai/vision/captioning.md new file mode 100644 index 000000000..604c1f336 --- /dev/null +++ b/docs/ai/vision/captioning.md @@ -0,0 +1,38 @@ +# Generating images + +Marvin can use OpenAI's vision API to process images as inputs. + +
+

What it does

+

+ The caption function generates text from images. +

+
+ + + +!!! example + + Generate a description of the following image, hypothetically available at `/path/to/marvin.webp`: + + ![](/assets/images/core/vision/marvin.webp) + + + ```python + import marvin + from pathlib import Path + + marvin.caption(image=Path('/path/to/marvin.webp')) + ``` + + !!! success "Result" + "This is a digital illustration featuring a stylized, cute character resembling a Funko Pop vinyl figure with large, shiny eyes and a square-shaped head, sitting on abstract wavy shapes that simulate a landscape. The whimsical figure is set against a dark background with sparkling, colorful bokeh effects, giving it a magical, dreamy atmosphere." + + +
+

How it works

+

+ Marvin passes your images to the OpenAI vision API as part of a larger prompt. +

+
+ diff --git a/docs/api_reference/ai/beta/vision.md b/docs/api_reference/ai/beta/vision.md new file mode 100644 index 000000000..196493a7c --- /dev/null +++ b/docs/api_reference/ai/beta/vision.md @@ -0,0 +1,6 @@ +# Vision tools + +!!! tip "Beta" + Please note that vision support in Marvin is still in beta, as OpenAI has not finalized the vision API yet. While it works as expected, it is subject to change. + +::: marvin.ai.beta.vision \ No newline at end of file diff --git a/docs/api_reference/requests.md b/docs/api_reference/requests.md deleted file mode 100644 index 962a508d5..000000000 --- a/docs/api_reference/requests.md +++ /dev/null @@ -1 +0,0 @@ -::: marvin.requests \ No newline at end of file diff --git a/docs/api_reference/types.md b/docs/api_reference/types.md new file mode 100644 index 000000000..c9091398e --- /dev/null +++ b/docs/api_reference/types.md @@ -0,0 +1 @@ +::: marvin.types \ No newline at end of file diff --git a/docs/assets/images/core/vision/marvin.webp b/docs/assets/images/core/vision/marvin.webp new file mode 100644 index 0000000000000000000000000000000000000000..f40ea7d152536dde3642120915eb4f8b0ef96f98 GIT binary patch literal 53182 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 Q@jVd$0Q$`;H}=2lKS4L}djJ3c literal 0 HcmV?d00001 diff --git a/mkdocs.yml b/mkdocs.yml index 3b648c797..3eafe1678 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -15,18 +15,20 @@ nav: # - Overview: welcome/overview.md - Text and data: - - AI functions: ai/function.md - - Structured data: ai/casting.md - - Entity extraction: ai/extraction.md - - Classification: ai/classification.md - - Generating synthetic data: ai/generation.md + - AI functions: ai/text/function.md + - Structured data: ai/text/casting.md + - Entity extraction: ai/text/extraction.md + - Classification: ai/text/classification.md + - Generating synthetic data: ai/text/generation.md - Images: - - Creating images: ai/painting.md - # - Captioning: ai/function.md + - Creating images: ai/images/painting.md + + - Vision: + - Captioning: ai/vision/captioning.md - Audio: - - Text-to-speech: ai/speech.md + - Text-to-speech: ai/audio/speech.md # - Transcription: ai/function.md - Configuration: @@ -42,8 +44,11 @@ nav: - marvin.ai.images: api_reference/ai/images.md - marvin.ai.audio: api_reference/ai/audio.md + - Beta AI modules: + - marvin.ai.beta.vision: api_reference/ai/beta/vision.md + - Object schemas: - - marvin.requests: api_reference/requests.md + - marvin.types: api_reference/types.md - Settings: - marvin.settings: api_reference/settings.md - Utilities: diff --git a/src/marvin/__init__.py b/src/marvin/__init__.py index 2f7343c34..7ad9fc8fc 100644 --- a/src/marvin/__init__.py +++ b/src/marvin/__init__.py @@ -3,6 +3,7 @@ from .ai.text import fn, cast, extract, classify, classifier, generate, model, Model from .ai.images import paint, image from .ai.audio import speak, speech +from .ai.beta.vision import caption try: from ._version import version as __version__ @@ -26,6 +27,8 @@ # --- audio --- "speak", "speech", + # --- vision (beta) --- + "caption", ] diff --git a/src/marvin/ai/beta/__init__.py b/src/marvin/ai/beta/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/marvin/ai/beta/vision.py b/src/marvin/ai/beta/vision.py new file mode 100644 index 000000000..082b3af36 --- /dev/null +++ b/src/marvin/ai/beta/vision.py @@ -0,0 +1,115 @@ +from pathlib import Path +from typing import ( + TypeVar, + Union, +) + +from pydantic import BaseModel + +import marvin +import marvin.utilities.tools +from marvin.ai.prompts.vision_prompts import CAPTION_PROMPT +from marvin.client.openai import MarvinClient +from marvin.types import ( + BaseMessage, + ChatResponse, + MessageImageURLContent, + VisionRequest, +) +from marvin.utilities.images import image_to_base64 +from marvin.utilities.jinja import Transcript +from marvin.utilities.logging import get_logger + +T = TypeVar("T") +M = TypeVar("M", bound=BaseModel) + +logger = get_logger(__name__) + + +def generate_llm_response( + prompt_template: str, + images: list[Union[str, Path]], + prompt_kwargs: dict = None, + model_kwargs: dict = None, +) -> ChatResponse: + """ + Generates a language model response based on a provided prompt template. + + This function uses a language model to generate a response based on a + provided prompt template. The function supports additional arguments for the + prompt and the language model. + + Args: + prompt_template (str): The template for the prompt. + images (list[Union[str, Path]]): The images to be + used in the prompt. Can be either URLs or local paths. + prompt_kwargs (dict, optional): Additional keyword arguments + for the prompt. Defaults to None. + model_kwargs (dict, optional): Additional keyword arguments + for the language model. Defaults to None. + + Returns: + ChatResponse: The generated response from the language model. + """ + model_kwargs = model_kwargs or {} + prompt_kwargs = prompt_kwargs or {} + messages = Transcript(content=prompt_template).render_to_messages(**prompt_kwargs) + + if images is not None: + for image in images: + # if images are local paths, convert them to base64. Otherwise + # assume they are URLs + if isinstance(image, Path): + b64_image = image_to_base64(image) + url = f"data:image/jpeg;base64,{b64_image}" + else: + url = image + + messages.append( + BaseMessage( + role="user", + content=[MessageImageURLContent(image_url=dict(url=url))], + ) + ) + + request = VisionRequest(messages=messages, **model_kwargs) + if marvin.settings.log_verbose: + logger.debug_kv("Request", request.model_dump_json(indent=2)) + response = MarvinClient().generate_vision( + **request.model_dump(exclude_none=True, exclude_unset=True) + ) + if marvin.settings.log_verbose: + logger.debug_kv("Response", response.model_dump_json(indent=2)) + return ChatResponse(request=request, response=response) + + +def caption( + image: Union[str, Path], + instructions: str = None, + model_kwargs: dict = None, +) -> str: + """ + Generates a caption for an image. + + This function uses a language model to generate a caption for an image. The + function supports additional arguments for the language model. + + Args: + image (Union[str, Path]): The URL or local path of the + image to be captioned. + instructions (str, optional): Specific instructions for + the caption. Defaults to None. + model_kwargs (dict, optional): Additional keyword + arguments for the language model. Defaults to None. + + Returns: + str: The generated caption. + """ + model_kwargs = model_kwargs or {} + response = generate_llm_response( + prompt_template=CAPTION_PROMPT, + images=[image], + prompt_kwargs=dict(instructions=instructions), + model_kwargs=model_kwargs, + ) + return response.response.choices[0].message.content diff --git a/src/marvin/ai/prompts/vision_prompts.py b/src/marvin/ai/prompts/vision_prompts.py new file mode 100644 index 000000000..d0e720ac7 --- /dev/null +++ b/src/marvin/ai/prompts/vision_prompts.py @@ -0,0 +1,15 @@ +import inspect + +CAPTION_PROMPT = inspect.cleandoc( + """ + Generate a descriptive caption the following image, and pay attention to any + additional instructions. Do not respond directly to the user ("you"), as + your response will become the input for other text processing functions. + + {% if instructions -%} + ## Instructions + + {{ instructions }} + {% endif %} + """ +) diff --git a/src/marvin/ai/text.py b/src/marvin/ai/text.py index 1f108f41e..3f06ee9ca 100644 --- a/src/marvin/ai/text.py +++ b/src/marvin/ai/text.py @@ -44,9 +44,7 @@ def generate_llm_response( - prompt_template: str, - prompt_kwargs: dict = None, - model_kwargs: dict = None, + prompt_template: str, prompt_kwargs: dict = None, model_kwargs: dict = None ) -> ChatResponse: """ Generates a language model response based on a provided prompt template. @@ -65,6 +63,7 @@ def generate_llm_response( model_kwargs = model_kwargs or {} prompt_kwargs = prompt_kwargs or {} messages = Transcript(content=prompt_template).render_to_messages(**prompt_kwargs) + request = ChatRequest(messages=messages, **model_kwargs) if marvin.settings.log_verbose: logger.debug_kv("Request", request.model_dump_json(indent=2)) diff --git a/src/marvin/client/openai.py b/src/marvin/client/openai.py index 8421e093a..73becd06b 100644 --- a/src/marvin/client/openai.py +++ b/src/marvin/client/openai.py @@ -18,7 +18,7 @@ import marvin from marvin import settings -from marvin.types import ChatRequest, ImageRequest +from marvin.types import ChatRequest, ImageRequest, VisionRequest if TYPE_CHECKING: from openai._base_client import HttpxBinaryResponseContent @@ -70,7 +70,21 @@ def generate_chat( ) # validate request request = ChatRequest(**kwargs) - response: "ChatCompletion" = create(**request.model_dump()) + response: "ChatCompletion" = create(**request.model_dump(exclude_none=True)) + return response + + def generate_vision( + self, + *, + completion: Optional[Callable[..., "ChatCompletion"]] = None, + **kwargs: Any, + ) -> Union["ChatCompletion", T]: + create: Callable[..., "ChatCompletion"] = ( + completion or self.client.chat.completions.create + ) + # validate request + request = VisionRequest(**kwargs) + response: "ChatCompletion" = create(**request.model_dump(exclude_none=True)) return response def generate_image( @@ -79,7 +93,7 @@ def generate_image( ) -> "ImagesResponse": # validate request request = ImageRequest(**marvin.settings.openai.images.model_dump() | kwargs) - return self.client.images.generate(**request.model_dump()) + return self.client.images.generate(**request.model_dump(exclude_none=True)) def generate_speech( self, @@ -119,7 +133,23 @@ async def generate_chat( create = self.client.chat.completions.create # validate request request = ChatRequest(**kwargs) - response: "ChatCompletion" = await create(request.model_dump()) + response: "ChatCompletion" = await create(request.model_dump(exclude_none=True)) + return response + + async def generate_vision( + self, + *, + completion: Optional[Callable[..., "ChatCompletion"]] = None, + **kwargs: Any, + ) -> Union["ChatCompletion", T]: + create: Callable[..., "ChatCompletion"] = ( + completion or self.client.chat.completions.create + ) + # validate request + request = VisionRequest(**kwargs) + response: "ChatCompletion" = await create( + **request.model_dump(exclude_none=True) + ) return response async def generate_image( @@ -128,7 +158,9 @@ async def generate_image( ) -> "ImagesResponse": # validate request request = ImageRequest(**marvin.settings.openai.images.model_dump() | kwargs) - return await self.client.images.generate(**request.model_dump()) + return await self.client.images.generate( + **request.model_dump(exclude_none=True) + ) async def generate_audio( self, diff --git a/src/marvin/settings.py b/src/marvin/settings.py index 5d1de248e..85eab6e23 100644 --- a/src/marvin/settings.py +++ b/src/marvin/settings.py @@ -56,8 +56,24 @@ def encoder(self): return tiktoken.encoding_for_model(self.model).encode +class ChatVisionSettings(MarvinSettings): + model_config = SettingsConfigDict(env_prefix="marvin_chat_vision_") + model: str = Field( + description="The default vision model to use.", default="gpt-4-vision-preview" + ) + temperature: float = Field(description="The default temperature to use.", default=1) + max_tokens: int = 500 + + @property + def encoder(self): + import tiktoken + + return tiktoken.encoding_for_model(self.model).encode + + class ChatSettings(MarvinSettings): completions: ChatCompletionSettings = Field(default_factory=ChatCompletionSettings) + vision: ChatVisionSettings = Field(default_factory=ChatVisionSettings) class ImageSettings(MarvinSettings): diff --git a/src/marvin/types.py b/src/marvin/types.py index 10bb35edc..f4389b467 100644 --- a/src/marvin/types.py +++ b/src/marvin/types.py @@ -63,8 +63,31 @@ class FunctionCall(BaseModel): name: str +class ImageUrl(BaseModel): + url: str = Field( + description="URL of the image to be sent or a base64 encoded image." + ) + detail: str = "auto" + + +class MessageImageURLContent(BaseModel): + """Schema for messages containing images""" + + type: Literal["image_url"] = "image_url" + image_url: ImageUrl + + +class MessageTextContent(BaseModel): + """Schema for messages containing text""" + + type: Literal["text"] = "text" + text: str + + class BaseMessage(BaseModel): - content: str + """Base schema for messages""" + + content: Union[str, list[Union[MessageImageURLContent, MessageTextContent]]] role: str @@ -103,9 +126,33 @@ class ChatRequest(Prompt[T]): user: Optional[str] = None +class VisionRequest(BaseModel): + messages: list[BaseMessage] = Field(default_factory=list) + model: str = Field(default_factory=lambda: settings.openai.chat.vision.model) + logit_bias: Optional[LogitBias] = None + max_tokens: Optional[Annotated[int, Field(strict=True, ge=1)]] = Field( + default_factory=lambda: settings.openai.chat.vision.max_tokens + ) + frequency_penalty: Optional[ + Annotated[float, Field(strict=True, ge=-2.0, le=2.0)] + ] = 0 + n: Optional[Annotated[int, Field(strict=True, ge=1)]] = 1 + presence_penalty: Optional[ + Annotated[float, Field(strict=True, ge=-2.0, le=2.0)] + ] = 0 + seed: Optional[int] = None + stop: Optional[Union[str, list[str]]] = None + stream: Optional[bool] = False + temperature: Optional[Annotated[float, Field(strict=True, ge=0, le=2)]] = Field( + default_factory=lambda: settings.openai.chat.vision.temperature + ) + top_p: Optional[Annotated[float, Field(strict=True, ge=0, le=1)]] = 1 + user: Optional[str] = None + + class ChatResponse(BaseModel): model_config = dict(arbitrary_types_allowed=True) - request: ChatRequest + request: Union[ChatRequest, VisionRequest] response: ChatCompletion tool_outputs: list[Any] = [] @@ -113,6 +160,7 @@ class ChatResponse(BaseModel): class ImageRequest(BaseModel): prompt: str model: Optional[str] = Field(default_factory=lambda: settings.openai.images.model) + n: Optional[int] = 1 quality: Optional[Literal["standard", "hd"]] = Field( default_factory=lambda: settings.openai.images.quality diff --git a/src/marvin/utilities/images.py b/src/marvin/utilities/images.py new file mode 100644 index 000000000..e8ace81e6 --- /dev/null +++ b/src/marvin/utilities/images.py @@ -0,0 +1,15 @@ +import base64 + + +def image_to_base64(image_path: str) -> str: + """ + Converts a local image file to a base64 string. + + Args: + image_path (str): The path to the image file. + + Returns: + str: The base64 representation of the image. + """ + with open(image_path, "rb") as image_file: + return base64.b64encode(image_file.read()).decode("utf-8") From a89b882a60ddee780e76e66ffe98435e68b97562 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Fri, 12 Jan 2024 10:00:46 -0500 Subject: [PATCH 2/8] Add beta note --- docs/ai/vision/captioning.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/ai/vision/captioning.md b/docs/ai/vision/captioning.md index 604c1f336..e58d92312 100644 --- a/docs/ai/vision/captioning.md +++ b/docs/ai/vision/captioning.md @@ -2,6 +2,9 @@ Marvin can use OpenAI's vision API to process images as inputs. +!!! tip "Beta" + Please note that vision support in Marvin is still in beta, as OpenAI has not finalized the vision API yet. While it works as expected, it is subject to change. +

What it does

From 853041af10a50a343ab9fca698a1874fa95daaae Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Fri, 12 Jan 2024 10:11:09 -0500 Subject: [PATCH 3/8] Update captioning.md --- docs/ai/vision/captioning.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/ai/vision/captioning.md b/docs/ai/vision/captioning.md index e58d92312..67da84ce6 100644 --- a/docs/ai/vision/captioning.md +++ b/docs/ai/vision/captioning.md @@ -16,7 +16,7 @@ Marvin can use OpenAI's vision API to process images as inputs. !!! example - Generate a description of the following image, hypothetically available at `/path/to/marvin.webp`: + Generate a description of the following image, hypothetically available at `/path/to/marvin.jpg`: ![](/assets/images/core/vision/marvin.webp) @@ -25,7 +25,7 @@ Marvin can use OpenAI's vision API to process images as inputs. import marvin from pathlib import Path - marvin.caption(image=Path('/path/to/marvin.webp')) + marvin.caption(image=Path('/path/to/marvin.jpg')) ``` !!! success "Result" From 9a03e8ea4a669a2d5508ca4a56e5d53e5a686575 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Fri, 12 Jan 2024 10:16:59 -0500 Subject: [PATCH 4/8] Update signature --- src/marvin/utilities/images.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/marvin/utilities/images.py b/src/marvin/utilities/images.py index e8ace81e6..94c38647d 100644 --- a/src/marvin/utilities/images.py +++ b/src/marvin/utilities/images.py @@ -1,12 +1,15 @@ import base64 +from pathlib import Path +from typing import Union -def image_to_base64(image_path: str) -> str: +def image_to_base64(image_path: Union[str, Path]) -> str: """ Converts a local image file to a base64 string. Args: - image_path (str): The path to the image file. + image_path (Union[str, Path]): The path to the image file. This can be a + string or a Path object. Returns: str: The base64 representation of the image. From 5f7db6e1ea6a655625aad7b25d716deb1af0925f Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Fri, 12 Jan 2024 10:30:10 -0500 Subject: [PATCH 5/8] Update src/marvin/ai/prompts/vision_prompts.py Co-authored-by: nate nowack --- src/marvin/ai/prompts/vision_prompts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/marvin/ai/prompts/vision_prompts.py b/src/marvin/ai/prompts/vision_prompts.py index d0e720ac7..bb79ce2b5 100644 --- a/src/marvin/ai/prompts/vision_prompts.py +++ b/src/marvin/ai/prompts/vision_prompts.py @@ -2,7 +2,7 @@ CAPTION_PROMPT = inspect.cleandoc( """ - Generate a descriptive caption the following image, and pay attention to any + Generate a descriptive caption for the following image, and pay attention to any additional instructions. Do not respond directly to the user ("you"), as your response will become the input for other text processing functions. From f8791567ffdb3b72992f6eaa12d90c91a81f0c61 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Fri, 12 Jan 2024 10:30:17 -0500 Subject: [PATCH 6/8] Update src/marvin/ai/text.py Co-authored-by: nate nowack --- src/marvin/ai/text.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/marvin/ai/text.py b/src/marvin/ai/text.py index 3f06ee9ca..5e60948a7 100644 --- a/src/marvin/ai/text.py +++ b/src/marvin/ai/text.py @@ -44,7 +44,7 @@ def generate_llm_response( - prompt_template: str, prompt_kwargs: dict = None, model_kwargs: dict = None + prompt_template: str, prompt_kwargs: Optional[dict] = None, model_kwargs: Optional[dict] = None ) -> ChatResponse: """ Generates a language model response based on a provided prompt template. From e803a95ccf077fdb39cb7c15ffdea3afc0c2b031 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Fri, 12 Jan 2024 11:09:14 -0500 Subject: [PATCH 7/8] Update text.py --- src/marvin/ai/text.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/marvin/ai/text.py b/src/marvin/ai/text.py index 5e60948a7..f902f5604 100644 --- a/src/marvin/ai/text.py +++ b/src/marvin/ai/text.py @@ -10,6 +10,7 @@ Callable, GenericAlias, Literal, + Optional, Type, TypeVar, Union, @@ -44,7 +45,9 @@ def generate_llm_response( - prompt_template: str, prompt_kwargs: Optional[dict] = None, model_kwargs: Optional[dict] = None + prompt_template: str, + prompt_kwargs: Optional[dict] = None, + model_kwargs: Optional[dict] = None, ) -> ChatResponse: """ Generates a language model response based on a provided prompt template. From 712a1d5355ead03c9a9794d9b81d3c04960cec78 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Fri, 12 Jan 2024 11:12:04 -0500 Subject: [PATCH 8/8] Update test_extract.py --- tests/apis/test_extract.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/apis/test_extract.py b/tests/apis/test_extract.py index cab6e6d6e..168fceb36 100644 --- a/tests/apis/test_extract.py +++ b/tests/apis/test_extract.py @@ -44,7 +44,11 @@ def test_extract_names(self): ) assert result == ["John", "Mary", "Bob"] + @pytest.mark.flaky(max_runs=3) def test_float_to_int(self): + # gpt 3.5 sometimes struggles with this test, marked as flaky + # pydantic no longer casts floats to ints, but gpt-3.5 assumes it's + # ok even when given instructions not to. GPT-4 seems to work ok. result = marvin.extract("the numbers are 1, 2, and 3.2", int) assert result == [1, 2, 3]