From 030acf47933c6fa118704cae8a8edac5176add0d Mon Sep 17 00:00:00 2001 From: Nathan Nowack Date: Sun, 31 Dec 2023 13:48:57 -0600 Subject: [PATCH 1/7] validator for api key --- src/marvin/client/openai.py | 44 +++++++++++++++++++++++++------------ src/marvin/settings.py | 13 ++++++++++- 2 files changed, 42 insertions(+), 15 deletions(-) diff --git a/src/marvin/client/openai.py b/src/marvin/client/openai.py index 24d166c5c..8ab33d39b 100644 --- a/src/marvin/client/openai.py +++ b/src/marvin/client/openai.py @@ -31,6 +31,34 @@ T = TypeVar("T", bound=pydantic.BaseModel) +def _get_default_client(client_type: str) -> Union[Client, AsyncClient]: + api_key = ( + settings.openai.api_key.get_secret_value() if settings.openai.api_key else None + ) + + if not api_key: + raise ValueError( + "OpenAI API key not set - please set `MARVIN_OPENAI_API_KEY` in `~/.marvin/.env`." + ) + + if client_type == "sync": + return Client( + **settings.openai.model_dump( + exclude={"chat", "images", "audio", "assistants", "api_key"} + ) + | dict(api_key=api_key) + ) + elif client_type == "async": + return AsyncClient( + **settings.openai.model_dump( + exclude={"chat", "images", "audio", "assistants", "api_key"} + ) + | dict(api_key=api_key) + ) + else: + raise ValueError(f"Invalid client type {client_type!r}") + + def with_response_model( create: Callable[P, "ChatCompletion"], ) -> Callable[ @@ -82,14 +110,7 @@ class MarvinClient(pydantic.BaseModel): arbitrary_types_allowed=True, protected_namespaces=() ) - client: Client = pydantic.Field( - default_factory=lambda: Client( - **settings.openai.model_dump( - exclude={"chat", "images", "audio", "assistants", "api_key"} - ) - | dict(api_key=settings.openai.api_key.get_secret_value()) - ) - ) + client: Client = pydantic.Field(default_factory=lambda: _get_default_client("sync")) @classmethod def wrap(cls, client: Client) -> "Client": @@ -170,12 +191,7 @@ class AsyncMarvinClient(pydantic.BaseModel): ) client: AsyncClient = pydantic.Field( - default_factory=lambda: AsyncClient( - **settings.openai.model_dump( - exclude={"chat", "images", "audio", "assistants", "api_key"} - ) - | dict(api_key=settings.openai.api_key.get_secret_value()) - ) + default_factory=lambda: _get_default_client("async") ) @classmethod diff --git a/src/marvin/settings.py b/src/marvin/settings.py index 585fb28f8..00170c053 100644 --- a/src/marvin/settings.py +++ b/src/marvin/settings.py @@ -15,7 +15,7 @@ from copy import deepcopy from typing import Any, Optional, Union -from pydantic import Field, SecretStr +from pydantic import Field, SecretStr, field_validator from pydantic_settings import BaseSettings, SettingsConfigDict from typing_extensions import Literal @@ -187,6 +187,17 @@ class OpenAISettings(MarvinSettings): audio: AudioSettings = Field(default_factory=AudioSettings) assistants: AssistantSettings = Field(default_factory=AssistantSettings) + @field_validator("api_key") + def discover_api_key(cls, v): + if v is None: + v = SecretStr(os.environ.get("OPENAI_API_KEY")) + if v.get_secret_value() is None: + raise ValueError( + "OpenAI API key not found. Please either set `MARVIN_OPENAI_API_KEY` in `~/.marvin/.env`" + " or otherwise set `OPENAI_API_KEY` in your environment." + ) + return v + class Settings(MarvinSettings): """Settings for `marvin`. From 4bf599dc86405bc544aab9e9d8a088ff9b0d59a6 Mon Sep 17 00:00:00 2001 From: Nathan Nowack Date: Sun, 31 Dec 2023 13:53:00 -0600 Subject: [PATCH 2/7] standardize error message --- src/marvin/client/openai.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/marvin/client/openai.py b/src/marvin/client/openai.py index 8ab33d39b..76ca05d35 100644 --- a/src/marvin/client/openai.py +++ b/src/marvin/client/openai.py @@ -38,9 +38,9 @@ def _get_default_client(client_type: str) -> Union[Client, AsyncClient]: if not api_key: raise ValueError( - "OpenAI API key not set - please set `MARVIN_OPENAI_API_KEY` in `~/.marvin/.env`." + "OpenAI API key not found. Please either set `MARVIN_OPENAI_API_KEY` in `~/.marvin/.env`" + " or otherwise set `OPENAI_API_KEY` in your environment." ) - if client_type == "sync": return Client( **settings.openai.model_dump( From fc1d8ceea81ca7fe3f556253e0dc31fe8a6bbf8b Mon Sep 17 00:00:00 2001 From: Nathan Nowack Date: Sun, 31 Dec 2023 13:54:52 -0600 Subject: [PATCH 3/7] more concise --- src/marvin/client/openai.py | 29 +++++++++++------------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/src/marvin/client/openai.py b/src/marvin/client/openai.py index 76ca05d35..539cd7ff3 100644 --- a/src/marvin/client/openai.py +++ b/src/marvin/client/openai.py @@ -35,29 +35,22 @@ def _get_default_client(client_type: str) -> Union[Client, AsyncClient]: api_key = ( settings.openai.api_key.get_secret_value() if settings.openai.api_key else None ) - if not api_key: raise ValueError( - "OpenAI API key not found. Please either set `MARVIN_OPENAI_API_KEY` in `~/.marvin/.env`" - " or otherwise set `OPENAI_API_KEY` in your environment." - ) - if client_type == "sync": - return Client( - **settings.openai.model_dump( - exclude={"chat", "images", "audio", "assistants", "api_key"} - ) - | dict(api_key=api_key) - ) - elif client_type == "async": - return AsyncClient( - **settings.openai.model_dump( - exclude={"chat", "images", "audio", "assistants", "api_key"} - ) - | dict(api_key=api_key) + "OpenAI API key not found. Set `MARVIN_OPENAI_API_KEY` in `~/.marvin/.env`" + " or `OPENAI_API_KEY` in environment." ) - else: + if client_type not in ["sync", "async"]: raise ValueError(f"Invalid client type {client_type!r}") + client_class = Client if client_type == "sync" else AsyncClient + return client_class( + **settings.openai.model_dump( + exclude={"chat", "images", "audio", "assistants", "api_key"} + ) + | {"api_key": api_key} + ) + def with_response_model( create: Callable[P, "ChatCompletion"], From a201ae574001781bb465ea9327909f94f550fa1c Mon Sep 17 00:00:00 2001 From: Nathan Nowack Date: Sun, 31 Dec 2023 13:56:16 -0600 Subject: [PATCH 4/7] standardize error message again --- src/marvin/client/openai.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/marvin/client/openai.py b/src/marvin/client/openai.py index 539cd7ff3..31d57771f 100644 --- a/src/marvin/client/openai.py +++ b/src/marvin/client/openai.py @@ -37,8 +37,8 @@ def _get_default_client(client_type: str) -> Union[Client, AsyncClient]: ) if not api_key: raise ValueError( - "OpenAI API key not found. Set `MARVIN_OPENAI_API_KEY` in `~/.marvin/.env`" - " or `OPENAI_API_KEY` in environment." + "OpenAI API key not found. Please either set `MARVIN_OPENAI_API_KEY` in `~/.marvin/.env`" + " or otherwise set `OPENAI_API_KEY` in your environment." ) if client_type not in ["sync", "async"]: raise ValueError(f"Invalid client type {client_type!r}") From f9bd7c5897cf54c929a146660511019f4473037f Mon Sep 17 00:00:00 2001 From: Nathan Nowack Date: Sun, 31 Dec 2023 14:13:54 -0600 Subject: [PATCH 5/7] update quickstart docs --- docs/configuration/settings.md | 15 ++++++-------- docs/welcome/quickstart.md | 36 +++++++++++++++++----------------- 2 files changed, 24 insertions(+), 27 deletions(-) diff --git a/docs/configuration/settings.md b/docs/configuration/settings.md index ca3d78e6f..042b54def 100644 --- a/docs/configuration/settings.md +++ b/docs/configuration/settings.md @@ -5,12 +5,13 @@ Marvin makes use of Pydantic's `BaseSettings` to configure, load, and change beh ## Environment Variables All settings are configurable via environment variables like `MARVIN_`. +Please set Marvin specific settings in `~/.marvin/.env`. One exception being `OPENAI_API_KEY`, which may be as a global env var on your system and it will be picked up by Marvin. + !!! example "Setting Environment Variables" - For example, in an `.env` file or in your shell config file you might have: + For example, in your `~/.marvin/.env` file or in your shell config file you might have: ```shell - MARVIN_LOG_LEVEL=DEBUG - MARVIN_LLM_MODEL=gpt-4 - MARVIN_LLM_TEMPERATURE=0 + MARVIN_LOG_LEVEL=INFO + MARVIN_OPENAI_CHAT_COMPLETIONS_MODEL=gpt-4 MARVIN_OPENAI_API_KEY='sk-my-api-key' ``` Settings these values will let you avoid setting an API key every time. @@ -23,10 +24,6 @@ A runtime settings object is accessible via `marvin.settings` and can be used to ```python import marvin - marvin.settings.llm_model # 'gpt-4' - - marvin.settings.llm_model = 'gpt-3.5-turbo' - - marvin.settings.llm_model # 'gpt-3.5-turbo' + marvin.settings.openai_chat_completions_model = 'gpt-4' ``` diff --git a/docs/welcome/quickstart.md b/docs/welcome/quickstart.md index 9ab29eab6..07389dc55 100644 --- a/docs/welcome/quickstart.md +++ b/docs/welcome/quickstart.md @@ -5,6 +5,18 @@ After [installing Marvin](../installation), the fastest way to get started is by !!! info "Initializing a Client" To use Marvin you must have an API Key configured for an external model provider, like OpenAI. + You can pass your API Key to Marvin in one of two ways: + + - Set the environment variable `MARVIN_OPENAI_API_KEY` in `~/.marvin/.env` or as `OPENAI_API_KEY` in your shell config file. + + ```shell + ยป cat ~/.marvin/.env | rg OPENAI + MARVIN_OPENAI_API_KEY=sk-xxx + MARVIN_OPENAI_ORGANIZATION=org-xxx + ``` + + - Pass your API Key to Marvin's `OpenAI` client constructor. + ```python from openai import OpenAI @@ -23,11 +35,8 @@ Marvin's most basic component is the AI Model, built on Pydantic's `BaseModel`. ```python from marvin import ai_model from pydantic import BaseModel, Field - from openai import OpenAI - client = OpenAI(api_key = 'YOUR_API_KEY') - - @ai_model(client = client) + @ai_model class Location(BaseModel): city: str state_abbreviation: str = Field( @@ -120,9 +129,6 @@ Marvin's most basic component is the AI Model, built on Pydantic's `BaseModel`. ```python from marvin import ai_model from pydantic import BaseModel, Field - from openai import OpenAI - - client = OpenAI(api_key = 'YOUR_API_KEY') class Location(BaseModel): city: str @@ -131,12 +137,12 @@ Marvin's most basic component is the AI Model, built on Pydantic's `BaseModel`. description="The two-letter state abbreviation" ) - ai_model(Location, client = client)("The Big Apple") + ai_model(Location)("The Big Apple") ``` ??? info "Generated Prompt" You can view and/or eject the generated prompt by simply calling ```python - ai_model(Location, client = client)("The Big Apple").as_prompt().serialize() + ai_model(Location)("The Big Apple").as_prompt().serialize() ``` When you do you'll see the raw payload that's sent to the LLM. All of the parameters below like `FormatResponse` and the prompt you send are fully customizable. @@ -358,11 +364,8 @@ AI Functions look like regular functions, but have no source code. Instead, an A `ai_fn` can decorate python functions to evlaute them using a Large Language Model. ```python from marvin import ai_fn - from openai import OpenAI - client = OpenAI(api_key = 'YOUR_API_KEY') - - @ai_fn(client=client) + @ai_fn def sentiment_list(texts: list[str]) -> list[float]: """ Given a list of `texts`, returns a list of numbers between 1 (positive) and @@ -437,9 +440,6 @@ AI Functions look like regular functions, but have no source code. Instead, an A `ai_fn` can be used as a utility function to evaluate python functions using a Large Language Model. ```python from marvin import ai_fn - from openai import OpenAI - - client = OpenAI(api_key = 'YOUR_API_KEY') def sentiment_list(texts: list[str]) -> list[float]: """ @@ -448,7 +448,7 @@ AI Functions look like regular functions, but have no source code. Instead, an A """ - ai_fn(sentiment_list, client=client)( + ai_fn(sentiment_list)( [ "That was surprisingly easy!", "Oh no, not again.", @@ -458,7 +458,7 @@ AI Functions look like regular functions, but have no source code. Instead, an A ??? info "Generated Prompt" You can view and/or eject the generated prompt by simply calling ```python - ai_fn(sentiment_list, client = client)([ + ai_fn(sentiment_list)([ "That was surprisingly easy!", "Oh no, not again.", ]).as_prompt().serialize() From 6eca13f501a02f5c4f23be87c1f21b44e637caf7 Mon Sep 17 00:00:00 2001 From: Nathan Nowack Date: Sun, 31 Dec 2023 14:16:41 -0600 Subject: [PATCH 6/7] update quickstart docs --- docs/static/css/tailwind.css | 10 +++++++--- docs/welcome/quickstart.md | 9 ++++++++- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/docs/static/css/tailwind.css b/docs/static/css/tailwind.css index f528103cb..e0705a726 100644 --- a/docs/static/css/tailwind.css +++ b/docs/static/css/tailwind.css @@ -1,5 +1,5 @@ /* -! tailwindcss v3.3.6 | MIT License | https://tailwindcss.com +! tailwindcss v3.4.0 | MIT License | https://tailwindcss.com */ /* @@ -32,9 +32,11 @@ 4. Use the user's configured `sans` font-family by default. 5. Use the user's configured `sans` font-feature-settings by default. 6. Use the user's configured `sans` font-variation-settings by default. +7. Disable tap highlights on iOS */ -html { +html, +:host { line-height: 1.5; /* 1 */ -webkit-text-size-adjust: 100%; @@ -44,12 +46,14 @@ html { -o-tab-size: 4; tab-size: 4; /* 3 */ - font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; /* 4 */ font-feature-settings: normal; /* 5 */ font-variation-settings: normal; /* 6 */ + -webkit-tap-highlight-color: transparent; + /* 7 */ } /* diff --git a/docs/welcome/quickstart.md b/docs/welcome/quickstart.md index 07389dc55..ea1bf9573 100644 --- a/docs/welcome/quickstart.md +++ b/docs/welcome/quickstart.md @@ -15,12 +15,19 @@ After [installing Marvin](../installation), the fastest way to get started is by MARVIN_OPENAI_ORGANIZATION=org-xxx ``` - - Pass your API Key to Marvin's `OpenAI` client constructor. + - Pass your API Key to Marvin's `OpenAI` client constructor and pass it to Marvin's `ai_fn`, `ai_classifier`, or `ai_model` decorators. ```python + from marvin import ai_fn from openai import OpenAI client = OpenAI(api_key = 'YOUR_API_KEY') + + @ai_fn(client = client) + def list_fruits(n: int, color: str = 'red') -> list[str]: + """ + Generates a list of {{n}} {{color}} fruits. + """ ``` ## Components From c067e4c03942b3022a49e505f99601b85c73d8c2 Mon Sep 17 00:00:00 2001 From: Nathan Nowack Date: Sun, 31 Dec 2023 14:19:34 -0600 Subject: [PATCH 7/7] word --- docs/configuration/settings.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration/settings.md b/docs/configuration/settings.md index 042b54def..1fe9ee3b9 100644 --- a/docs/configuration/settings.md +++ b/docs/configuration/settings.md @@ -8,7 +8,7 @@ All settings are configurable via environment variables like `MARVIN_