feat: add API key configuration and handling for agents and tools
This commit is contained in:
+28
@@ -12,4 +12,32 @@ A powerful, flexible Agent Based LLM tool for creating and managing networks of
|
||||
:target: https://travis-ci.org/cleverthis/cleveragents
|
||||
:alt: Travis-CI Build Status
|
||||
|
||||
API Key Configuration
|
||||
---------------------
|
||||
|
||||
CleverAgents requires API keys for certain agents and tools (e.g., LLM providers, web search). Keys can be configured in two ways, with the agent-specific configuration taking precedence.
|
||||
|
||||
1. **In Agent Configuration**:
|
||||
Set the ``api_key`` field directly in the agent or tool definition within your YAML configuration file. If this key is present and not empty, it will always be used.
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
agents:
|
||||
- name: my_openai_agent
|
||||
type: llm
|
||||
config:
|
||||
provider: openai
|
||||
model: gpt-4
|
||||
api_key: "sk-your-key-from-config"
|
||||
|
||||
2. **Via Environment Variables**:
|
||||
If ``api_key`` is not set or is an empty string in the configuration, the application will look for a corresponding environment variable.
|
||||
|
||||
- **OpenAI**: ``OPENAI_API_KEY``
|
||||
- **Anthropic**: ``ANTHROPIC_API_KEY``
|
||||
- **Google Gemini**: ``GOOGLE_GEMINI_API_KEY``
|
||||
- **Google Search Tool**: ``GOOGLE_SEARCH_API_KEY``
|
||||
|
||||
If a required API key is not found in either location, the application will raise a ``ConfigurationError``.
|
||||
|
||||
Overview
|
||||
|
||||
@@ -37,6 +37,12 @@ class LLMAgent(AgentWithMemory):
|
||||
generate responses. It supports different LLM providers and models, and
|
||||
handles prompt formatting and response processing.
|
||||
|
||||
API keys are resolved in the following order of precedence:
|
||||
1. A non-empty `api_key` in the agent's configuration.
|
||||
2. The corresponding environment variable (e.g., `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`).
|
||||
|
||||
If a required key is not found, a `ConfigurationError` is raised.
|
||||
|
||||
Attributes:
|
||||
name (str): The name of the agent.
|
||||
config (Dict[str, Any]): The agent's configuration.
|
||||
@@ -72,9 +78,7 @@ class LLMAgent(AgentWithMemory):
|
||||
# Extract configuration
|
||||
self.provider = self.agent_config.get("provider", "openai").lower()
|
||||
self.model = self.agent_config.get("model", "gpt-3.5-turbo")
|
||||
self.api_key = self.agent_config.get(
|
||||
"api_key", os.environ.get(f"{self.provider.upper()}_API_KEY")
|
||||
)
|
||||
self.api_key = self._get_api_key()
|
||||
# Always prioritise the configured system message, falling back to the default.
|
||||
self.system_message = self.agent_config.get(
|
||||
"system_message", DEFAULT_SYSTEM_MESSAGE
|
||||
@@ -82,14 +86,50 @@ class LLMAgent(AgentWithMemory):
|
||||
self.temperature = float(self.agent_config.get("temperature", 0.7))
|
||||
self.max_tokens = int(self.agent_config.get("max_tokens", 1000))
|
||||
|
||||
# Check that we have an API key
|
||||
if not self.api_key:
|
||||
raise AgentCreationError(f"No API key provided for {self.provider} LLM")
|
||||
|
||||
# Initialize memory for conversation history (system message is injected
|
||||
# fresh on every API call to avoid stale configuration issues).
|
||||
self.memory["messages"] = []
|
||||
|
||||
def _get_api_key(self) -> str:
|
||||
"""
|
||||
Retrieves the API key from config or environment variables.
|
||||
|
||||
The method follows this priority:
|
||||
1. A non-empty `api_key` in the agent's configuration.
|
||||
2. The corresponding environment variable (e.g., OPENAI_API_KEY).
|
||||
|
||||
Raises:
|
||||
ConfigurationError: If no API key can be found for a provider that requires one.
|
||||
|
||||
Returns:
|
||||
The API key string.
|
||||
"""
|
||||
config_api_key = self.agent_config.get("api_key")
|
||||
if config_api_key and config_api_key.strip():
|
||||
return config_api_key.strip()
|
||||
|
||||
provider_env_map = {
|
||||
"openai": "OPENAI_API_KEY",
|
||||
"anthropic": "ANTHROPIC_API_KEY",
|
||||
"gemini": "GOOGLE_GEMINI_API_KEY",
|
||||
}
|
||||
env_var_name = provider_env_map.get(self.provider)
|
||||
|
||||
if env_var_name:
|
||||
env_api_key = os.environ.get(env_var_name)
|
||||
if env_api_key:
|
||||
return env_api_key
|
||||
|
||||
if self.provider in provider_env_map:
|
||||
raise ConfigurationError(
|
||||
f"API key for provider '{self.provider}' not found. "
|
||||
f"Please set it in the agent configuration or via the "
|
||||
f"'{env_var_name}' environment variable."
|
||||
)
|
||||
|
||||
# This case is for providers that do not require an API key.
|
||||
return ""
|
||||
|
||||
async def process(
|
||||
self, message: str, context: Optional[Dict[str, Any]] = None
|
||||
) -> str:
|
||||
|
||||
@@ -20,6 +20,7 @@ import aiohttp
|
||||
|
||||
from cleveragents.agents.base import Agent
|
||||
from cleveragents.core.exceptions import AgentCreationError
|
||||
from cleveragents.core.exceptions import ConfigurationError
|
||||
from cleveragents.core.exceptions import ExecutionError
|
||||
from cleveragents.templates.renderer import TemplateRenderer
|
||||
|
||||
@@ -120,7 +121,8 @@ class WebSearchTool(Tool):
|
||||
A tool for searching the web using Google Custom Search.
|
||||
|
||||
This tool performs web searches and returns the results. It requires a Google
|
||||
API Key and a Programmable Search Engine ID (cx).
|
||||
API Key and a Programmable Search Engine ID (cx). The API key can be provided
|
||||
in the configuration or via the GOOGLE_SEARCH_API_KEY environment variable.
|
||||
|
||||
Attributes:
|
||||
api_key (str): The API key for the search engine.
|
||||
@@ -150,23 +152,52 @@ class WebSearchTool(Tool):
|
||||
f"Search engine '{self.search_engine}' is not supported. Only 'google' is available."
|
||||
)
|
||||
|
||||
# API key can be in config or from environment variable GOOGLE_API_KEY
|
||||
self.api_key = config.get("api_key") or os.environ.get("GOOGLE_API_KEY")
|
||||
self.api_key = self._get_api_key()
|
||||
# CX (Custom Search Engine ID) can be in config or from environment variable GOOGLE_CX
|
||||
self.cx = config.get("cx") or os.environ.get("GOOGLE_CX")
|
||||
|
||||
if not self.api_key:
|
||||
raise AgentCreationError(
|
||||
"Google API Key not found. Please provide it in the tool config "
|
||||
"or set the GOOGLE_API_KEY environment variable."
|
||||
)
|
||||
|
||||
if not self.cx:
|
||||
raise AgentCreationError(
|
||||
"Google Custom Search Engine ID (cx) not found. Please provide it "
|
||||
"in the tool config or set the GOOGLE_CX environment variable."
|
||||
)
|
||||
|
||||
def _get_api_key(self) -> str:
|
||||
"""
|
||||
Retrieves the API key from config or environment variables.
|
||||
|
||||
The method follows this priority:
|
||||
1. A non-empty `api_key` in the tool's configuration.
|
||||
2. The corresponding environment variable (e.g., GOOGLE_SEARCH_API_KEY).
|
||||
|
||||
Raises:
|
||||
ConfigurationError: If no API key can be found for a search engine that requires one.
|
||||
|
||||
Returns:
|
||||
The API key string.
|
||||
"""
|
||||
config_api_key = self.config.get("api_key")
|
||||
if config_api_key and config_api_key.strip():
|
||||
return config_api_key.strip()
|
||||
|
||||
engine_env_map = {
|
||||
"google": "GOOGLE_SEARCH_API_KEY",
|
||||
}
|
||||
env_var_name = engine_env_map.get(self.search_engine)
|
||||
|
||||
if env_var_name:
|
||||
env_api_key = os.environ.get(env_var_name)
|
||||
if env_api_key:
|
||||
return env_api_key
|
||||
|
||||
if self.search_engine in engine_env_map:
|
||||
raise ConfigurationError(
|
||||
f"API key for search engine '{self.search_engine}' not found. "
|
||||
f"Please set it in the tool configuration or via the "
|
||||
f"'{env_var_name}' environment variable."
|
||||
)
|
||||
return ""
|
||||
|
||||
async def execute(
|
||||
self, input_data: str, context: Optional[Dict[str, Any]] = None
|
||||
) -> str:
|
||||
|
||||
@@ -29,7 +29,6 @@ agents:
|
||||
config:
|
||||
model: gpt-4
|
||||
provider: openai
|
||||
api_key: ${OPENAI_API_KEY}
|
||||
temperature: 0.3
|
||||
prompt_template: technical_analysis_prompt
|
||||
|
||||
@@ -38,7 +37,6 @@ agents:
|
||||
config:
|
||||
model: gpt-3.5-turbo
|
||||
provider: openai
|
||||
api_key: ${OPENAI_API_KEY}
|
||||
temperature: 0.4
|
||||
prompt_template: summary_prompt
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ agents:
|
||||
config:
|
||||
model: claude-3-opus
|
||||
provider: anthropic
|
||||
api_key: ${ANTHROPIC_API_KEY}
|
||||
temperature: 0.2
|
||||
prompt_template: analysis_prompt
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ agents:
|
||||
config:
|
||||
model: gpt-4
|
||||
provider: openai
|
||||
api_key: ${OPENAI_API_KEY}
|
||||
temperature: 0.7
|
||||
prompt_template: research_prompt
|
||||
|
||||
@@ -20,7 +19,6 @@ agents:
|
||||
config:
|
||||
model: claude-3-opus-latest
|
||||
provider: anthropic
|
||||
api_key: ${ANTHROPIC_API_KEY}
|
||||
temperature: 0.2
|
||||
prompt_template: financial_analysis_prompt
|
||||
|
||||
@@ -29,7 +27,6 @@ agents:
|
||||
config:
|
||||
model: gpt-4
|
||||
provider: openai
|
||||
api_key: ${OPENAI_API_KEY}
|
||||
temperature: 0.3
|
||||
prompt_template: technical_analysis_prompt
|
||||
|
||||
@@ -38,7 +35,6 @@ agents:
|
||||
config:
|
||||
model: gpt-3.5-turbo
|
||||
provider: openai
|
||||
api_key: ${OPENAI_API_KEY}
|
||||
temperature: 0.4
|
||||
prompt_template: summary_prompt
|
||||
|
||||
|
||||
@@ -29,7 +29,6 @@ agents:
|
||||
config:
|
||||
provider: openai
|
||||
model: gpt-3.5-turbo
|
||||
api_key: "${OPENAI_API_KEY}" # Uses environment variable for security
|
||||
system_message: |
|
||||
You are a classification expert. Your task is to analyze the user's prompt and determine its primary intent.
|
||||
Respond with ONLY ONE of the following keywords based on the prompt:
|
||||
@@ -42,7 +41,6 @@ agents:
|
||||
config:
|
||||
provider: openai
|
||||
model: gpt-3.5-turbo
|
||||
api_key: "${OPENAI_API_KEY}" # Uses environment variable for security
|
||||
system_message: |
|
||||
The user is providing a prompt that requires a calculation to be performed. Respond with ONLY the
|
||||
calculation that needs to be performed without any words or texts present. Make sure the response uses
|
||||
@@ -65,7 +63,6 @@ agents:
|
||||
config:
|
||||
provider: openai
|
||||
model: gpt-4o-mini
|
||||
api_key: "${OPENAI_API_KEY}"
|
||||
system_message: |
|
||||
You are a response synthesizer. Your job is to take the raw output from a tool which performs the
|
||||
the mathematical calculation present in a users response, along with the users original prompt, and
|
||||
@@ -88,7 +85,6 @@ agents:
|
||||
config:
|
||||
provider: openai
|
||||
model: gpt-4o-mini
|
||||
api_key: "${OPENAI_API_KEY}"
|
||||
system_message: |
|
||||
You are friendly and helpful assistant. Your task is to analyze the user's prompt and determine if you
|
||||
have enough knowledge to answer the question like an expert on the topic.
|
||||
@@ -112,7 +108,6 @@ agents:
|
||||
config:
|
||||
provider: openai
|
||||
model: gpt-4o-mini
|
||||
api_key: "${OPENAI_API_KEY}"
|
||||
system_message: |
|
||||
You are a response synthesizer. Your job is to take the raw output from a web search, along with the
|
||||
original prompt a user provided, which the web search is relevant to, and provide an appropriate
|
||||
@@ -132,7 +127,6 @@ agents:
|
||||
config:
|
||||
provider: openai
|
||||
model: gpt-4o-mini
|
||||
api_key: "${OPENAI_API_KEY}"
|
||||
system_message: "You are a friendly and helpful assistant. Provide clear and concise answers to the user's questions."
|
||||
temperature: 0.5
|
||||
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
Feature: API Key Handling for Agents and Tools
|
||||
|
||||
Scenario: LLM Agent uses API key from config when present
|
||||
Given an LLM agent config for provider "openai" with api_key "config-key"
|
||||
And the environment variable "OPENAI_API_KEY" is set to "env-key"
|
||||
When I initialize the LLM agent with config
|
||||
Then the agent's API key should be "config-key"
|
||||
|
||||
Scenario: LLM Agent falls back to environment variable when config key is missing
|
||||
Given an LLM agent config for provider "openai" with api_key "null"
|
||||
And the environment variable "OPENAI_API_KEY" is set to "env-key"
|
||||
When I initialize the LLM agent with config
|
||||
Then the agent's API key should be "env-key"
|
||||
|
||||
Scenario: LLM Agent falls back to environment variable when config key is empty
|
||||
Given an LLM agent config for provider "openai" with api_key "empty"
|
||||
And the environment variable "OPENAI_API_KEY" is set to "env-key"
|
||||
When I initialize the LLM agent with config
|
||||
Then the agent's API key should be "env-key"
|
||||
|
||||
Scenario: LLM Agent raises error if no API key is found
|
||||
Given an LLM agent config for provider "openai" with api_key "null"
|
||||
And the environment variable "OPENAI_API_KEY" is unset
|
||||
When I initialize the LLM agent with config
|
||||
Then a ConfigurationError should be raised containing "OPENAI_API_KEY"
|
||||
|
||||
Scenario: WebSearchTool uses API key from environment variable
|
||||
Given the environment variable "GOOGLE_SEARCH_API_KEY" is set to "env-key-for-search"
|
||||
When I initialize a WebSearchTool with a missing API key
|
||||
Then the tool's API key should be "env-key-for-search"
|
||||
|
||||
Scenario: WebSearchTool raises error if no API key is found
|
||||
Given the environment variable "GOOGLE_SEARCH_API_KEY" is unset
|
||||
When I try to initialize a WebSearchTool with a missing API key
|
||||
Then a ConfigurationError should be raised containing "GOOGLE_SEARCH_API_KEY"
|
||||
@@ -237,3 +237,77 @@ def step_impl(context):
|
||||
assert context.processed_response.startswith(
|
||||
"Formatted response:"
|
||||
), f"Response was not formatted correctly: {context.processed_response}"
|
||||
|
||||
|
||||
import os
|
||||
from unittest.mock import patch
|
||||
|
||||
from behave import given
|
||||
from behave import then
|
||||
from behave import when
|
||||
|
||||
from cleveragents.agents.llm import LLMAgent
|
||||
from cleveragents.core.exceptions import ConfigurationError
|
||||
from cleveragents.templates.renderer import TemplateRenderer
|
||||
|
||||
|
||||
@given('the environment variable "{name}" is set to "{value}"')
|
||||
def step_env_set(context, name, value):
|
||||
if not hasattr(context, "env_patchers"):
|
||||
context.env_patchers = []
|
||||
patcher = patch.dict(os.environ, {name: value})
|
||||
patcher.start()
|
||||
context.env_patchers.append(patcher)
|
||||
|
||||
|
||||
@given('the environment variable "{name}" is unset')
|
||||
def step_env_unset(context, name):
|
||||
if not hasattr(context, "env_patchers"):
|
||||
context.env_patchers = []
|
||||
patcher = patch.dict(os.environ)
|
||||
patcher.start()
|
||||
if name in os.environ:
|
||||
del os.environ[name]
|
||||
context.env_patchers.append(patcher)
|
||||
|
||||
|
||||
@given('an LLM agent config for provider "{provider}" with api_key "{api_key}"')
|
||||
def step_llm_config(context, provider, api_key):
|
||||
context.template_renderer = TemplateRenderer()
|
||||
context.llm_config = {
|
||||
"provider": provider,
|
||||
"model": "test-model",
|
||||
}
|
||||
if api_key == "null":
|
||||
pass # Don't add the key to the config dict
|
||||
elif api_key == "empty":
|
||||
context.llm_config["api_key"] = ""
|
||||
else:
|
||||
context.llm_config["api_key"] = api_key
|
||||
|
||||
|
||||
@when("I initialize the LLM agent with config")
|
||||
def step_initialize_llm(context):
|
||||
context.error = None
|
||||
try:
|
||||
context.agent = LLMAgent(
|
||||
"test_agent", {"config": context.llm_config}, context.template_renderer
|
||||
)
|
||||
except Exception as e:
|
||||
context.error = e
|
||||
|
||||
|
||||
@then('the agent\'s API key should be "{api_key}"')
|
||||
def step_check_api_key(context, api_key):
|
||||
assert not context.error, f"Agent initialization failed with: {context.error}"
|
||||
assert hasattr(context, "agent"), "Agent was not initialized."
|
||||
assert context.agent.api_key == api_key
|
||||
|
||||
|
||||
@then('a ConfigurationError should be raised containing "{text}"')
|
||||
def step_check_configuration_error(context, text):
|
||||
assert context.error, "Expected a ConfigurationError, but no exception was raised."
|
||||
assert isinstance(
|
||||
context.error, ConfigurationError
|
||||
), f"Expected ConfigurationError, but got {type(context.error).__name__}."
|
||||
assert text in str(context.error)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import asyncio
|
||||
from unittest.mock import MagicMock
|
||||
from unittest.mock import patch
|
||||
|
||||
from behave import given
|
||||
from behave import then
|
||||
@@ -89,3 +88,25 @@ def step_impl(context):
|
||||
assert context.results["format1"] == "2"
|
||||
assert "Web search results for 'CleverAgents'" in context.results["format2"]
|
||||
assert "Web search results for" in context.results["format3"]
|
||||
|
||||
|
||||
from cleveragents.core.exceptions import ConfigurationError
|
||||
|
||||
|
||||
@when("I initialize a WebSearchTool with a missing API key")
|
||||
@when("I try to initialize a WebSearchTool with a missing API key")
|
||||
def step_initialize_web_search_missing_api(context):
|
||||
context.error = None
|
||||
try:
|
||||
# Config is missing the 'api_key'
|
||||
config = {"search_engine": "google", "cx": "test-cx"}
|
||||
context.tool = WebSearchTool(config)
|
||||
except Exception as e:
|
||||
context.error = e
|
||||
|
||||
|
||||
@then('the tool\'s API key should be "{api_key}"')
|
||||
def step_check_tool_api_key(context, api_key):
|
||||
assert not context.error, f"Tool initialization failed with: {context.error}"
|
||||
assert hasattr(context, "tool")
|
||||
assert context.tool.api_key == api_key
|
||||
|
||||
@@ -56,3 +56,8 @@ def after_scenario(context, scenario):
|
||||
if hasattr(context, "error"):
|
||||
print(f"Error: {context.error}")
|
||||
print("===============================\n")
|
||||
|
||||
if hasattr(context, "env_patchers"):
|
||||
for patcher in reversed(context.env_patchers):
|
||||
patcher.stop()
|
||||
delattr(context, "env_patchers")
|
||||
|
||||
Reference in New Issue
Block a user