chore: Add goose providers list command (#116)
Co-authored-by: Bradley Axen <baxen@squareup.com>
This commit is contained in:
@@ -7,7 +7,7 @@ from exchange import Message, Tool
|
||||
from exchange.content import Text, ToolResult, ToolUse
|
||||
from exchange.providers.base import Provider, Usage
|
||||
from tenacity import retry, wait_fixed, stop_after_attempt
|
||||
from exchange.providers.utils import get_provider_env_value, retry_if_status, raise_for_status
|
||||
from exchange.providers.utils import retry_if_status, raise_for_status
|
||||
|
||||
ANTHROPIC_HOST = "https://api.anthropic.com/v1/messages"
|
||||
|
||||
@@ -20,13 +20,19 @@ retry_procedure = retry(
|
||||
|
||||
|
||||
class AnthropicProvider(Provider):
|
||||
"""Provides chat completions for models hosted directly by Anthropic."""
|
||||
|
||||
PROVIDER_NAME = "anthropic"
|
||||
REQUIRED_ENV_VARS = ["ANTHROPIC_API_KEY"]
|
||||
|
||||
def __init__(self, client: httpx.Client) -> None:
|
||||
self.client = client
|
||||
|
||||
@classmethod
|
||||
def from_env(cls: Type["AnthropicProvider"]) -> "AnthropicProvider":
|
||||
cls.check_env_vars()
|
||||
url = os.environ.get("ANTHROPIC_HOST", ANTHROPIC_HOST)
|
||||
key = get_provider_env_value("ANTHROPIC_API_KEY", "anthropic")
|
||||
key = os.environ.get("ANTHROPIC_API_KEY")
|
||||
client = httpx.Client(
|
||||
base_url=url,
|
||||
headers={
|
||||
|
||||
@@ -1,26 +1,32 @@
|
||||
from typing import Type
|
||||
|
||||
import httpx
|
||||
import os
|
||||
|
||||
from exchange.providers import OpenAiProvider
|
||||
from exchange.providers.utils import get_provider_env_value
|
||||
|
||||
PROVIDER_NAME = "azure"
|
||||
|
||||
|
||||
class AzureProvider(OpenAiProvider):
|
||||
"""Provides chat completions for models hosted by the Azure OpenAI Service"""
|
||||
"""Provides chat completions for models hosted by the Azure OpenAI Service."""
|
||||
|
||||
PROVIDER_NAME = "azure"
|
||||
REQUIRED_ENV_VARS = [
|
||||
"AZURE_CHAT_COMPLETIONS_HOST_NAME",
|
||||
"AZURE_CHAT_COMPLETIONS_DEPLOYMENT_NAME",
|
||||
"AZURE_CHAT_COMPLETIONS_DEPLOYMENT_API_VERSION",
|
||||
"AZURE_CHAT_COMPLETIONS_KEY",
|
||||
]
|
||||
|
||||
def __init__(self, client: httpx.Client) -> None:
|
||||
super().__init__(client)
|
||||
|
||||
@classmethod
|
||||
def from_env(cls: Type["AzureProvider"]) -> "AzureProvider":
|
||||
url = get_provider_env_value("AZURE_CHAT_COMPLETIONS_HOST_NAME", PROVIDER_NAME)
|
||||
deployment_name = get_provider_env_value("AZURE_CHAT_COMPLETIONS_DEPLOYMENT_NAME", PROVIDER_NAME)
|
||||
|
||||
api_version = get_provider_env_value("AZURE_CHAT_COMPLETIONS_DEPLOYMENT_API_VERSION", PROVIDER_NAME)
|
||||
key = get_provider_env_value("AZURE_CHAT_COMPLETIONS_KEY", PROVIDER_NAME)
|
||||
cls.check_env_vars()
|
||||
url = os.environ.get("AZURE_CHAT_COMPLETIONS_HOST_NAME")
|
||||
deployment_name = os.environ.get("AZURE_CHAT_COMPLETIONS_DEPLOYMENT_NAME")
|
||||
api_version = os.environ.get("AZURE_CHAT_COMPLETIONS_DEPLOYMENT_API_VERSION")
|
||||
key = os.environ.get("AZURE_CHAT_COMPLETIONS_KEY")
|
||||
|
||||
# format the url host/"openai/deployments/" + deployment_name + "/?api-version=" + api_version
|
||||
url = f"{url}/openai/deployments/{deployment_name}/"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import os
|
||||
from abc import ABC, abstractmethod
|
||||
from attrs import define, field
|
||||
from typing import List, Optional, Tuple, Type
|
||||
@@ -14,10 +15,19 @@ class Usage:
|
||||
|
||||
|
||||
class Provider(ABC):
|
||||
PROVIDER_NAME: str
|
||||
REQUIRED_ENV_VARS: list[str] = []
|
||||
|
||||
@classmethod
|
||||
def from_env(cls: Type["Provider"]) -> "Provider":
|
||||
return cls()
|
||||
|
||||
@classmethod
|
||||
def check_env_vars(cls: Type["Provider"], instructions_url: Optional[str] = None) -> None:
|
||||
for env_var in cls.REQUIRED_ENV_VARS:
|
||||
if env_var not in os.environ:
|
||||
raise MissingProviderEnvVariableError(env_var, cls.PROVIDER_NAME, instructions_url)
|
||||
|
||||
@abstractmethod
|
||||
def complete(
|
||||
self,
|
||||
|
||||
@@ -13,7 +13,7 @@ from exchange.content import Text, ToolResult, ToolUse
|
||||
from exchange.message import Message
|
||||
from exchange.providers import Provider, Usage
|
||||
from tenacity import retry, wait_fixed, stop_after_attempt
|
||||
from exchange.providers.utils import get_provider_env_value, raise_for_status, retry_if_status
|
||||
from exchange.providers.utils import raise_for_status, retry_if_status
|
||||
from exchange.tool import Tool
|
||||
|
||||
SERVICE = "bedrock-runtime"
|
||||
@@ -146,19 +146,26 @@ class AwsClient(httpx.Client):
|
||||
return headers
|
||||
|
||||
|
||||
PROVIDER_NAME = "bedrock"
|
||||
|
||||
|
||||
class BedrockProvider(Provider):
|
||||
"""Provides chat completions for models hosted by the Amazon Bedrock Service"""
|
||||
|
||||
PROVIDER_NAME = "bedrock"
|
||||
REQUIRED_ENV_VARS = [
|
||||
"AWS_ACCESS_KEY_ID",
|
||||
"AWS_SECRET_ACCESS_KEY",
|
||||
"AWS_SESSION_TOKEN",
|
||||
]
|
||||
|
||||
def __init__(self, client: AwsClient) -> None:
|
||||
self.client = client
|
||||
|
||||
@classmethod
|
||||
def from_env(cls: Type["BedrockProvider"]) -> "BedrockProvider":
|
||||
cls.check_env_vars()
|
||||
aws_region = os.environ.get("AWS_REGION", "us-east-1")
|
||||
aws_access_key = get_provider_env_value("AWS_ACCESS_KEY_ID", PROVIDER_NAME)
|
||||
aws_secret_key = get_provider_env_value("AWS_SECRET_ACCESS_KEY", PROVIDER_NAME)
|
||||
aws_session_token = get_provider_env_value("AWS_SESSION_TOKEN", PROVIDER_NAME)
|
||||
aws_access_key = os.environ.get("AWS_ACCESS_KEY_ID")
|
||||
aws_secret_key = os.environ.get("AWS_SECRET_ACCESS_KEY")
|
||||
aws_session_token = os.environ.get("AWS_SESSION_TOKEN")
|
||||
|
||||
client = AwsClient(
|
||||
aws_region=aws_region,
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
from typing import Any, Dict, List, Tuple, Type
|
||||
|
||||
import httpx
|
||||
import os
|
||||
|
||||
from exchange.message import Message
|
||||
from exchange.providers.base import Provider, Usage
|
||||
from tenacity import retry, wait_fixed, stop_after_attempt
|
||||
from exchange.providers.utils import get_provider_env_value, raise_for_status, retry_if_status
|
||||
from exchange.providers.utils import raise_for_status, retry_if_status
|
||||
from exchange.providers.utils import (
|
||||
messages_to_openai_spec,
|
||||
openai_response_to_message,
|
||||
@@ -23,21 +24,29 @@ retry_procedure = retry(
|
||||
|
||||
|
||||
class DatabricksProvider(Provider):
|
||||
"""Provides chat completions for models on Databricks serving endpoints
|
||||
"""Provides chat completions for models on Databricks serving endpoints.
|
||||
|
||||
Models are expected to follow the llm/v1/chat "task". This includes support
|
||||
for foundation and external model endpoints
|
||||
https://docs.databricks.com/en/machine-learning/model-serving/create-foundation-model-endpoints.html#create-generative-ai-model-serving-endpoints
|
||||
|
||||
"""
|
||||
|
||||
PROVIDER_NAME = "databricks"
|
||||
REQUIRED_ENV_VARS = [
|
||||
"DATABRICKS_HOST",
|
||||
"DATABRICKS_TOKEN",
|
||||
]
|
||||
instructions_url = "https://docs.databricks.com/en/dev-tools/auth/index.html#general-host-token-and-account-id-environment-variables-and-fields"
|
||||
|
||||
def __init__(self, client: httpx.Client) -> None:
|
||||
super().__init__()
|
||||
self.client = client
|
||||
|
||||
@classmethod
|
||||
def from_env(cls: Type["DatabricksProvider"]) -> "DatabricksProvider":
|
||||
url = cls._get_env_variable("DATABRICKS_HOST")
|
||||
key = cls._get_env_variable("DATABRICKS_TOKEN")
|
||||
cls.check_env_vars(cls.instructions_url)
|
||||
url = os.environ.get("DATABRICKS_HOST")
|
||||
key = os.environ.get("DATABRICKS_TOKEN")
|
||||
client = httpx.Client(
|
||||
base_url=url,
|
||||
auth=("token", key),
|
||||
@@ -89,8 +98,3 @@ class DatabricksProvider(Provider):
|
||||
json=payload,
|
||||
)
|
||||
return raise_for_status(response).json()
|
||||
|
||||
@classmethod
|
||||
def _get_env_variable(cls: Type["DatabricksProvider"], key: str) -> str:
|
||||
instruction = "https://docs.databricks.com/en/dev-tools/auth/index.html#general-host-token-and-account-id-environment-variables-and-fields"
|
||||
return get_provider_env_value(key, "databricks", instruction)
|
||||
|
||||
@@ -7,7 +7,7 @@ from exchange import Message, Tool
|
||||
from exchange.content import Text, ToolResult, ToolUse
|
||||
from exchange.providers.base import Provider, Usage
|
||||
from tenacity import retry, wait_fixed, stop_after_attempt
|
||||
from exchange.providers.utils import get_provider_env_value, raise_for_status, retry_if_status
|
||||
from exchange.providers.utils import raise_for_status, retry_if_status
|
||||
|
||||
GOOGLE_HOST = "https://generativelanguage.googleapis.com/v1beta"
|
||||
|
||||
@@ -20,15 +20,20 @@ retry_procedure = retry(
|
||||
|
||||
|
||||
class GoogleProvider(Provider):
|
||||
"""Provides chat completions for models hosted by Google, including Gemini and other experimental models."""
|
||||
|
||||
PROVIDER_NAME = "google"
|
||||
REQUIRED_ENV_VARS = ["GOOGLE_API_KEY"]
|
||||
instructions_url = "https://ai.google.dev/gemini-api/docs/api-key"
|
||||
|
||||
def __init__(self, client: httpx.Client) -> None:
|
||||
self.client = client
|
||||
|
||||
@classmethod
|
||||
def from_env(cls: Type["GoogleProvider"]) -> "GoogleProvider":
|
||||
cls.check_env_vars(cls.instructions_url)
|
||||
url = os.environ.get("GOOGLE_HOST", GOOGLE_HOST)
|
||||
api_key_instructions_url = "https://ai.google.dev/gemini-api/docs/api-key"
|
||||
key = get_provider_env_value("GOOGLE_API_KEY", "google", api_key_instructions_url)
|
||||
|
||||
key = os.environ.get("GOOGLE_API_KEY")
|
||||
client = httpx.Client(
|
||||
base_url=url,
|
||||
headers={
|
||||
|
||||
@@ -10,7 +10,7 @@ OLLAMA_MODEL = "mistral-nemo"
|
||||
|
||||
|
||||
class OllamaProvider(OpenAiProvider):
|
||||
"""Provides chat completions for models hosted by Ollama"""
|
||||
"""Provides chat completions for models hosted by Ollama."""
|
||||
|
||||
__doc__ += """Here's an example profile configuration to try:
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import httpx
|
||||
from exchange.message import Message
|
||||
from exchange.providers.base import Provider, Usage
|
||||
from exchange.providers.utils import (
|
||||
get_provider_env_value,
|
||||
messages_to_openai_spec,
|
||||
openai_response_to_message,
|
||||
openai_single_message_context_length_exceeded,
|
||||
@@ -28,17 +27,21 @@ retry_procedure = retry(
|
||||
|
||||
|
||||
class OpenAiProvider(Provider):
|
||||
"""Provides chat completions for models hosted directly by OpenAI"""
|
||||
"""Provides chat completions for models hosted directly by OpenAI."""
|
||||
|
||||
PROVIDER_NAME = "openai"
|
||||
REQUIRED_ENV_VARS = ["OPENAI_API_KEY"]
|
||||
instructions_url = "https://platform.openai.com/docs/api-reference/api-keys"
|
||||
|
||||
def __init__(self, client: httpx.Client) -> None:
|
||||
super().__init__()
|
||||
self.client = client
|
||||
|
||||
@classmethod
|
||||
def from_env(cls: Type["OpenAiProvider"]) -> "OpenAiProvider":
|
||||
cls.check_env_vars(cls.instructions_url)
|
||||
url = os.environ.get("OPENAI_HOST", OPENAI_HOST)
|
||||
api_key_instructions_url = "https://platform.openai.com/docs/api-reference/api-keys"
|
||||
key = get_provider_env_value("OPENAI_API_KEY", "openai", api_key_instructions_url)
|
||||
key = os.environ.get("OPENAI_API_KEY")
|
||||
|
||||
client = httpx.Client(
|
||||
base_url=url + "v1/",
|
||||
auth=("Bearer", key),
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
from typing import Any, Callable, Dict, List, Optional, Tuple
|
||||
|
||||
import httpx
|
||||
from exchange.content import Text, ToolResult, ToolUse
|
||||
from exchange.message import Message
|
||||
from exchange.providers.base import MissingProviderEnvVariableError
|
||||
from exchange.tool import Tool
|
||||
from tenacity import retry_if_exception
|
||||
|
||||
@@ -181,13 +179,6 @@ def openai_single_message_context_length_exceeded(error_dict: dict) -> None:
|
||||
raise InitialMessageTooLargeError(f"Input message too long. Message: {error_dict.get('message')}")
|
||||
|
||||
|
||||
def get_provider_env_value(env_variable: str, provider: str, instructions_url: Optional[str] = None) -> str:
|
||||
try:
|
||||
return os.environ[env_variable]
|
||||
except KeyError:
|
||||
raise MissingProviderEnvVariableError(env_variable, provider, instructions_url)
|
||||
|
||||
|
||||
class InitialMessageTooLargeError(Exception):
|
||||
"""Custom error raised when the first input message in an exchange is too large."""
|
||||
|
||||
|
||||
@@ -97,6 +97,28 @@ def list_toolkits() -> None:
|
||||
print(f" - [bold]{toolkit_name}[/bold]: {first_line_of_doc}")
|
||||
|
||||
|
||||
@goose_cli.group()
|
||||
def providers() -> None:
|
||||
"""Manage providers"""
|
||||
pass
|
||||
|
||||
|
||||
@providers.command(name="list")
|
||||
def list_providers() -> None:
|
||||
providers = load_plugins(group="exchange.provider")
|
||||
|
||||
for provider_name, provider in providers.items():
|
||||
lines_doc = provider.__doc__.split("\n")
|
||||
first_line_of_doc = lines_doc[0]
|
||||
print(f" - [bold]{provider_name}[/bold]: {first_line_of_doc}")
|
||||
envs = provider.REQUIRED_ENV_VARS
|
||||
if envs:
|
||||
env_required_str = ", ".join(envs)
|
||||
print(f" [dim]env vars required: {env_required_str}")
|
||||
|
||||
print("\n")
|
||||
|
||||
|
||||
def autocomplete_session_files(ctx: click.Context, args: str, incomplete: str) -> None:
|
||||
return [
|
||||
f"{session_name}"
|
||||
|
||||
@@ -10,3 +10,14 @@ def lint_toolkits() -> None:
|
||||
assert first_line_of_docstring[
|
||||
0
|
||||
].isupper(), f"`{toolkit_name}` toolkit docstring must start with a capital letter"
|
||||
|
||||
|
||||
def lint_providers() -> None:
|
||||
for provider_name, provider in load_plugins(group="exchange.provider").items():
|
||||
assert provider.__doc__ is not None, f"`{provider_name}` provider must have a docstring"
|
||||
first_line_of_docstring = provider.__doc__.split("\n")[0]
|
||||
assert len(first_line_of_docstring.split(" ")) > 5, f"`{provider_name}` provider docstring is too short"
|
||||
assert len(first_line_of_docstring.split(" ")) < 20, f"`{provider_name}` provider docstring is too long"
|
||||
assert first_line_of_docstring[
|
||||
0
|
||||
].isupper(), f"`{provider_name}` provider docstring must start with a capital letter"
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
from goose.toolkit.lint import lint_toolkits
|
||||
|
||||
from goose.toolkit.lint import lint_providers
|
||||
|
||||
|
||||
def test_lint_toolkits():
|
||||
lint_toolkits()
|
||||
|
||||
|
||||
def test_lint_providers():
|
||||
lint_providers()
|
||||
|
||||
Reference in New Issue
Block a user