feat: add API key configuration and handling for agents and tools

This commit is contained in:
2025-07-02 02:14:00 +00:00
parent 9392934fd0
commit 72e9fcb985
12 changed files with 251 additions and 30 deletions
+28
View File
@@ -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
+47 -7
View File
@@ -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:
+40 -9
View File
@@ -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:
-2
View File
@@ -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
-1
View File
@@ -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
-6
View File
@@ -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
+35
View File
@@ -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)
+22 -1
View File
@@ -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
+5
View File
@@ -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")