diff --git a/Makefile b/Makefile index 046f7f9..3adaeae 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,7 @@ all: help TEST_FILE ?= tests/unit_tests/ test: - TOOL_SERVER_API_KEY=placeholder TOOL_SERVER_URL=http://localhost:8080 uv run pytest $(TEST_FILE) + ANTHROPIC_API_KEY=placeholder TOOL_SERVER_API_KEY=placeholder TOOL_SERVER_URL=http://localhost:8080 uv run pytest $(TEST_FILE) ###################### diff --git a/README.md b/README.md index 2a4c46e..a22f673 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,10 @@ See the [example-tool-server](https://github.com/langchain-ai/example-tool-serve If you already have a tool server running, you can launch the ReAct agent with the following command: ```shell +# Specify at least one chat model API key! +export ANTHROPIC_API_KEY="..." +export OPENAI_API_KEY="..." +# Specify the tool server URL and API key TOOL_SERVER_API_KEY="Authorization header token" # This depends on how you have configured the tool server, and whether you have enabled authentication. if there's no authentication, just put a random string here. TOOL_SERVER_URL=http://localhost:8000 TOOL_SERVER_API_KEY=$TOOL_SERVER_API_KEY uv run langgraph dev ``` diff --git a/src/react_agent/configuration.py b/src/react_agent/configuration.py index 9362008..d9d9f98 100644 --- a/src/react_agent/configuration.py +++ b/src/react_agent/configuration.py @@ -2,6 +2,8 @@ from __future__ import annotations +import importlib +import os from dataclasses import dataclass, field, fields from typing import Annotated, Literal, Optional @@ -14,6 +16,63 @@ from react_agent import prompts, tools, utils # needs to be generated dynamically based on the available tools. APP_STATE = utils.State() +PROVIDER_SECRET_PACKAGE = [ + ("openai", "OPENAI_API_KEY", "langchain_openai"), + ("anthropic", "ANTHROPIC_API_KEY", "langchain_anthropic"), +] + + +def identify_available_model_providers() -> list[str]: + """Determine which model providers are available.""" + available_providers = [] + for provider, secret, package in PROVIDER_SECRET_PACKAGE: + if not os.environ.get(secret): + continue + + if not importlib.util.find_spec(package): + continue + + available_providers.append(provider) + + if not available_providers: + providers = ", ".join([p for p, _, _ in PROVIDER_SECRET_PACKAGE]) + + msg = ( + "Could not use any chat models. You must install the appropriate package " + "and set the required environment variables.\n" + f"You can use any of the following providers: {providers}\n" + "To do that you will need to install the package and set the required " + "environment variables." + ) + + for provider, secret, package in PROVIDER_SECRET_PACKAGE: + msg += ( + f"\n\nFor {provider} models, install the package '{package}' " + f"and set the environment variable '{secret}'." + ) + + raise ValueError(msg) + + return available_providers + + +PROVIDER_TO_MODELS = { + "openai": ["openai/o1", "openai/gpt-4o-mini", "openai/o1-mini", "openai/o3-mini"], + "anthropic": [ + "anthropic/claude-3-7-sonnet-latest", + "anthropic/claude-3-5-haiku-latest", + ], +} + + +def get_available_models() -> list[str]: + """Get the available models for the given providers.""" + available_models = [] + providers = identify_available_model_providers() + for provider in providers: + available_models.extend(PROVIDER_TO_MODELS[provider]) + return available_models + def create_configurable(toolbox: tools.Toolbox) -> None: """Dynamically create a configuration schema for the agent. @@ -25,6 +84,10 @@ def create_configurable(toolbox: tools.Toolbox) -> None: # to make the type information available when generating # the json schema for the configuration. APP_STATE.tool_names = toolbox.get_tool_names() + AVAILABLE_MODELS = get_available_models() + if len(AVAILABLE_MODELS) < 1: + raise ValueError("No models available for the agent.") + APP_STATE.available_model_names = AVAILABLE_MODELS @dataclass(kw_only=True) class Config: @@ -39,17 +102,10 @@ def create_configurable(toolbox: tools.Toolbox) -> None: ) model: Annotated[ - Literal[ - "anthropic/claude-3-7-sonnet-latest", - "anthropic/claude-3-5-haiku-latest", - "openai/o1", - "openai/gpt-4o-mini", - "openai/o1-mini", - "openai/o3-mini", - ], + Literal[*APP_STATE.available_model_names,], {"__template_metadata__": {"kind": "llm"}}, ] = field( - default="anthropic/claude-3-5-haiku-latest", + default=APP_STATE.available_model_names[0], metadata={ "description": ( "The name of the language model to use for the agent's "