Feats: Added google / Gemini provider
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
Feature: Google provider adapter coverage
|
||||
As a maintainer validating real provider adapters
|
||||
I want Behave scenarios for the Google chat provider
|
||||
So that constructor guards and LangChain handoffs stay regression tested
|
||||
|
||||
@unit @providers @google
|
||||
Scenario: Google provider instantiates ChatGoogleGenerativeAI with provided credentials
|
||||
Given I have sample provider domain inputs
|
||||
When I create a Google chat provider with API key "sk-google-unit" and model "gemini-2.0-flash"
|
||||
And I request plan generation from the Google provider
|
||||
Then the Google provider should construct ChatGoogleGenerativeAI with api key "sk-google-unit" and model "gemini-2.0-flash"
|
||||
And the Google provider metadata should report name "google" and model "gemini-2.0-flash"
|
||||
|
||||
@unit @providers @google
|
||||
Scenario: Google provider forwards extra kwargs to ChatGoogleGenerativeAI
|
||||
Given I have sample provider domain inputs
|
||||
And I set the Google provider extra kwargs "temperature=0.25,max_output_tokens=2048"
|
||||
When I create a Google chat provider with API key "sk-google-unit" and model "gemini-2.0-pro"
|
||||
And I request plan generation from the Google provider
|
||||
Then the Google provider should construct ChatGoogleGenerativeAI with api key "sk-google-unit" and model "gemini-2.0-pro"
|
||||
And the Google provider should include kwargs "temperature=0.25,max_output_tokens=2048" in the ChatGoogleGenerativeAI call
|
||||
And the Google provider metadata should report name "google" and model "gemini-2.0-pro"
|
||||
|
||||
@unit @providers @google
|
||||
Scenario: Google provider returns generated changes with token count
|
||||
Given I have sample provider domain inputs
|
||||
And the plan generation graph returns a generated change for "app/google_provider.py"
|
||||
And the Google provider token estimator returns 256 tokens
|
||||
When I create a Google chat provider with API key "sk-google-unit" and model "gemini-2.0-flash"
|
||||
And I request plan generation from the Google provider
|
||||
Then the Google provider response should include 1 generated change for "app/google_provider.py"
|
||||
And the Google provider response token count should equal 256
|
||||
|
||||
@unit @providers @google
|
||||
Scenario: Google provider rejects missing API key
|
||||
Given I have sample provider domain inputs
|
||||
When I attempt to create a Google chat provider without an API key
|
||||
Then the Google provider creation should fail with error "Google API key is required"
|
||||
@@ -359,6 +359,14 @@ Feature: Provider Registry Coverage
|
||||
And the stubbed OpenRouter client headers should include "HTTP-Referer" value "ExampleApp"
|
||||
And the stubbed OpenRouter client headers should include "X-Title" value "ExampleApp"
|
||||
|
||||
@unit @providers @registry
|
||||
Scenario: _create_provider_llm coerces OpenRouter default header mappings to strings
|
||||
Given I have a ProviderRegistry with "openrouter" API key set
|
||||
And the "OpenRouter" LangChain client is stubbed
|
||||
And the OpenRouter default headers include numeric entries
|
||||
When I call _create_provider_llm for provider "openrouter" with model "router-coerce" and custom default headers
|
||||
Then the stubbed OpenRouter client headers should include "123" value "456"
|
||||
|
||||
@unit @providers @registry
|
||||
Scenario: _create_provider_llm raises error for unsupported provider
|
||||
Given I have a ProviderRegistry instance
|
||||
@@ -379,6 +387,13 @@ Feature: Provider Registry Coverage
|
||||
Then a provider registry ValueError should be raised
|
||||
And the provider registry error should mention unknown provider type
|
||||
|
||||
@unit @providers @registry
|
||||
Scenario: Create AI provider raises error when Google API key is missing
|
||||
Given I have a ProviderRegistry with no API keys
|
||||
When I try to create an AI provider for provider "google"
|
||||
Then a provider registry ValueError should be raised
|
||||
And the provider registry error should mention missing "GOOGLE_API_KEY" environment variable
|
||||
|
||||
@unit @providers @registry
|
||||
Scenario: Create AI provider succeeds for configured provider string
|
||||
Given I have a ProviderRegistry with OpenAI API key set
|
||||
@@ -388,6 +403,15 @@ Feature: Provider Registry Coverage
|
||||
And the provider registry AI provider name should be "openai"
|
||||
And the provider registry AI provider model_id should be "gpt-4o"
|
||||
|
||||
@unit @providers @registry
|
||||
Scenario: Create AI provider returns Google adapter
|
||||
Given I have a ProviderRegistry with "google" API key set
|
||||
And CLEVERAGENTS_DEFAULT_MODEL is not set
|
||||
When I create an AI provider for provider "google"
|
||||
Then the provider registry AI provider should be a GoogleChatProvider
|
||||
And the provider registry AI provider name should be "google"
|
||||
And the provider registry AI provider model_id should be "gemini-2.0-flash"
|
||||
|
||||
@unit @providers @registry
|
||||
Scenario: Create AI provider without specifying provider uses fallback
|
||||
Given I have a ProviderRegistry with OpenAI API key set
|
||||
@@ -397,6 +421,7 @@ Feature: Provider Registry Coverage
|
||||
Then the provider registry AI provider should be a LangChainChatProvider
|
||||
And the provider registry AI provider name should be "openai"
|
||||
|
||||
|
||||
@unit @providers @registry
|
||||
Scenario: Create AI provider honors explicit model overrides
|
||||
Given I have a ProviderRegistry with OpenAI API key set
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from behave import given, then, when
|
||||
|
||||
from cleveragents.providers.llm.google_provider import GoogleChatProvider
|
||||
|
||||
|
||||
def _register_cleanup(context, cleanup):
|
||||
if hasattr(context, "add_cleanup"):
|
||||
context.add_cleanup(cleanup)
|
||||
else:
|
||||
cleanup_handlers = getattr(context, "_cleanup_handlers", [])
|
||||
cleanup_handlers.append(cleanup)
|
||||
context._cleanup_handlers = cleanup_handlers
|
||||
|
||||
|
||||
def _parse_kwargs_string(kwargs_string: str) -> dict[str, Any]:
|
||||
if not kwargs_string:
|
||||
return {}
|
||||
parsed: dict[str, Any] = {}
|
||||
for entry in kwargs_string.split(","):
|
||||
entry = entry.strip()
|
||||
if not entry:
|
||||
continue
|
||||
if "=" not in entry:
|
||||
parsed[entry] = True
|
||||
continue
|
||||
key, value = entry.split("=", 1)
|
||||
key = key.strip()
|
||||
value = value.strip()
|
||||
try:
|
||||
parsed_value = ast.literal_eval(value)
|
||||
except Exception:
|
||||
parsed_value = value
|
||||
parsed[key] = parsed_value
|
||||
return parsed
|
||||
|
||||
|
||||
def _setup_plan_generation_graph(context) -> MagicMock:
|
||||
patcher = patch(
|
||||
"cleveragents.providers.llm.langchain_chat_provider.PlanGenerationGraph"
|
||||
)
|
||||
context.plan_generation_patcher = patcher
|
||||
mock_graph_class = patcher.start()
|
||||
_register_cleanup(context, patcher.stop)
|
||||
mock_graph_instance = MagicMock(name="PlanGenerationGraphInstance")
|
||||
|
||||
default_state = {
|
||||
"generated_changes": [],
|
||||
"validation_result": {"status": "PASS"},
|
||||
"error": None,
|
||||
}
|
||||
state_override = getattr(context, "plan_generation_state_override", None)
|
||||
mock_graph_instance.invoke.return_value = state_override or default_state
|
||||
|
||||
invoke_side_effect = getattr(context, "plan_generation_invoke_side_effect", None)
|
||||
if invoke_side_effect is not None:
|
||||
mock_graph_instance.invoke.side_effect = invoke_side_effect
|
||||
|
||||
stream_events = getattr(context, "plan_generation_stream_events", None)
|
||||
if stream_events is None:
|
||||
mock_graph_instance.stream.return_value = iter(())
|
||||
else:
|
||||
events_copy = list(stream_events)
|
||||
|
||||
def _stream(*_args, **_kwargs):
|
||||
for event in events_copy:
|
||||
yield event
|
||||
|
||||
mock_graph_instance.stream.side_effect = _stream
|
||||
|
||||
mock_graph_class.return_value = mock_graph_instance
|
||||
context.plan_generation_graph = mock_graph_instance
|
||||
context.plan_generation_graph_class = mock_graph_class
|
||||
return mock_graph_instance
|
||||
|
||||
|
||||
@given('I set the Google provider extra kwargs "{kwargs_string}"')
|
||||
def step_set_google_provider_kwargs(context, kwargs_string):
|
||||
context.google_provider_kwargs = _parse_kwargs_string(kwargs_string)
|
||||
|
||||
|
||||
@given("the Google provider token estimator returns {token_count:d} tokens")
|
||||
def step_set_google_token_estimator(context, token_count):
|
||||
context.google_token_count = token_count
|
||||
|
||||
|
||||
@when('I create a Google chat provider with API key "{api_key}" and model "{model_id}"')
|
||||
def step_create_google_provider(context, api_key, model_id):
|
||||
patcher = patch("cleveragents.providers.llm.google_provider.ChatGoogleGenerativeAI")
|
||||
context.chat_google_patcher = patcher
|
||||
mock_chat_class = patcher.start()
|
||||
_register_cleanup(context, patcher.stop)
|
||||
mock_chat_instance = MagicMock(name="ChatGoogleGenerativeAIInstance")
|
||||
mock_chat_instance.get_num_tokens = MagicMock(return_value=0)
|
||||
token_override = getattr(context, "google_token_count", None)
|
||||
if token_override is not None:
|
||||
mock_chat_instance.get_num_tokens.return_value = token_override
|
||||
mock_chat_class.return_value = mock_chat_instance
|
||||
|
||||
extra_kwargs = dict(getattr(context, "google_provider_kwargs", {}))
|
||||
|
||||
context.provider = GoogleChatProvider(
|
||||
api_key=api_key,
|
||||
model=model_id,
|
||||
**extra_kwargs,
|
||||
)
|
||||
context.chat_google_class = mock_chat_class
|
||||
context.chat_google_instance = mock_chat_instance
|
||||
|
||||
|
||||
@when("I attempt to create a Google chat provider without an API key")
|
||||
def step_google_provider_without_api_key(context):
|
||||
context.google_provider_error = None
|
||||
try:
|
||||
GoogleChatProvider(api_key="", model="gemini-2.0-flash")
|
||||
except Exception as exc: # pragma: no cover - defensive
|
||||
context.google_provider_error = exc
|
||||
|
||||
|
||||
@when("I request plan generation from the Google provider")
|
||||
def step_request_google_plan_generation(context):
|
||||
_setup_plan_generation_graph(context)
|
||||
context.response = context.provider.generate_changes(
|
||||
context.project,
|
||||
context.plan,
|
||||
context.contexts,
|
||||
)
|
||||
context.chat_google_call = context.chat_google_class.call_args
|
||||
context.plan_generation_graph_call = context.plan_generation_graph_class.call_args
|
||||
|
||||
|
||||
@then(
|
||||
'the Google provider should construct ChatGoogleGenerativeAI with api key "{api_key}" and model "{model_id}"'
|
||||
)
|
||||
def step_assert_google_constructor(context, api_key, model_id):
|
||||
call = getattr(context, "chat_google_call", None)
|
||||
assert call is not None, "ChatGoogleGenerativeAI should have been called"
|
||||
call_args, call_kwargs = call
|
||||
assert call_args == (), "ChatGoogleGenerativeAI should use keyword arguments"
|
||||
assert call_kwargs["api_key"] == api_key
|
||||
assert call_kwargs["model"] == model_id
|
||||
|
||||
graph_call = getattr(context, "plan_generation_graph_call", None)
|
||||
assert graph_call is not None, "PlanGenerationGraph should receive the LLM"
|
||||
_, graph_kwargs = graph_call
|
||||
assert graph_kwargs["llm"] is context.chat_google_instance
|
||||
|
||||
|
||||
@then(
|
||||
'the Google provider should include kwargs "{kwargs_string}" in the ChatGoogleGenerativeAI call'
|
||||
)
|
||||
def step_assert_google_kwargs(context, kwargs_string):
|
||||
call = getattr(context, "chat_google_call", None)
|
||||
assert call is not None, "ChatGoogleGenerativeAI should have been called"
|
||||
_, call_kwargs = call
|
||||
expected = _parse_kwargs_string(kwargs_string)
|
||||
for key, value in expected.items():
|
||||
assert key in call_kwargs, f"Expected {key} in ChatGoogleGenerativeAI kwargs"
|
||||
assert call_kwargs[key] == value, (
|
||||
f"Expected ChatGoogleGenerativeAI kwargs[{key!r}] to equal {value!r}"
|
||||
)
|
||||
|
||||
|
||||
@then(
|
||||
'the Google provider metadata should report name "{expected_name}" and model "{expected_model}"'
|
||||
)
|
||||
def step_assert_google_metadata(context, expected_name, expected_model):
|
||||
provider = getattr(context, "provider", None)
|
||||
assert provider is not None, "Google provider should exist"
|
||||
assert provider.name == expected_name
|
||||
assert provider.model_id == expected_model
|
||||
response = getattr(context, "response", None)
|
||||
if response is not None:
|
||||
assert response.model_used == expected_model
|
||||
|
||||
|
||||
@then(
|
||||
'the Google provider response should include {count:d} generated change for "{file_path}"'
|
||||
)
|
||||
def step_assert_google_generated_change(context, count, file_path):
|
||||
response = getattr(context, "response", None)
|
||||
assert response is not None, "Provider response should exist"
|
||||
assert len(response.changes) == count, (
|
||||
f"Expected {count} changes, got {len(response.changes)}"
|
||||
)
|
||||
assert any(
|
||||
getattr(change, "file_path", None) == file_path for change in response.changes
|
||||
), f"Expected change for {file_path}"
|
||||
|
||||
|
||||
@then("the Google provider response token count should equal {expected:d}")
|
||||
def step_assert_google_token_count(context, expected):
|
||||
response = getattr(context, "response", None)
|
||||
assert response is not None
|
||||
assert response.token_count == expected
|
||||
|
||||
|
||||
@then('the Google provider creation should fail with error "{message}"')
|
||||
def step_assert_google_creation_error(context, message):
|
||||
error = getattr(context, "google_provider_error", None)
|
||||
assert error is not None, "Expected the provider to raise an error"
|
||||
assert isinstance(error, ValueError)
|
||||
assert str(error) == message
|
||||
@@ -5,14 +5,13 @@ from typing import Any
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from behave import given, then, when
|
||||
|
||||
from tenacity import RetryError, Retrying, stop_after_attempt
|
||||
|
||||
from cleveragents.domain.models.core import (
|
||||
Context,
|
||||
OperationType,
|
||||
Plan,
|
||||
Project,
|
||||
OperationType,
|
||||
)
|
||||
from cleveragents.providers.llm.openai_provider import OpenAIChatProvider
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ from unittest.mock import MagicMock
|
||||
|
||||
from behave import given, then, when # type: ignore[import-untyped]
|
||||
|
||||
from cleveragents.providers.llm.google_provider import GoogleChatProvider
|
||||
from cleveragents.providers.llm.langchain_chat_provider import LangChainChatProvider
|
||||
from cleveragents.providers.registry import (
|
||||
ProviderCapabilities,
|
||||
@@ -262,6 +263,11 @@ def step_impl_set_openrouter_org(context: Any, value: str) -> None:
|
||||
context.registry._settings.openrouter_organization = value
|
||||
|
||||
|
||||
@given("the OpenRouter default headers include numeric entries")
|
||||
def step_impl_set_openrouter_default_headers(context: Any) -> None:
|
||||
context.openrouter_default_headers = {123: 456, "X-Debug": 789}
|
||||
|
||||
|
||||
@given("the Azure endpoint setting is cleared")
|
||||
def step_impl_clear_azure_endpoint(context: Any) -> None:
|
||||
assert hasattr(context, "registry"), "Registry must exist before clearing endpoint"
|
||||
@@ -496,6 +502,21 @@ def step_impl_call_private_llm(context: Any, value: str, model: str) -> None:
|
||||
context.created_llm = context.registry._create_provider_llm(provider_type, model)
|
||||
|
||||
|
||||
@when(
|
||||
'I call _create_provider_llm for provider "{value}" with model "{model}" and custom default headers'
|
||||
)
|
||||
def step_impl_call_private_llm_with_headers(
|
||||
context: Any, value: str, model: str
|
||||
) -> None:
|
||||
provider_enum = _ensure_provider_type(context)
|
||||
provider_type = getattr(provider_enum, value.upper())
|
||||
default_headers = getattr(context, "openrouter_default_headers", None)
|
||||
assert default_headers is not None, "OpenRouter default headers were not configured"
|
||||
context.created_llm = context.registry._create_provider_llm(
|
||||
provider_type, model, default_headers=default_headers
|
||||
)
|
||||
|
||||
|
||||
@when('I try to call _create_provider_llm for provider "{value}" with model "{model}"')
|
||||
def step_impl_try_private_llm(context: Any, value: str, model: str) -> None:
|
||||
provider_enum = _ensure_provider_type(context)
|
||||
@@ -743,11 +764,27 @@ def step_impl_error_mentions_missing_endpoint(context: Any) -> None:
|
||||
assert "azure" in message and "endpoint" in message
|
||||
|
||||
|
||||
@then(
|
||||
'the provider registry error should mention missing "{env_var}" environment variable'
|
||||
)
|
||||
def step_impl_error_mentions_missing_env(context: Any, env_var: str) -> None:
|
||||
assert context.error is not None
|
||||
message = str(context.error)
|
||||
assert env_var in message, (
|
||||
f"Expected error message to mention {env_var!r}, got {message!r}"
|
||||
)
|
||||
|
||||
|
||||
@then("the provider registry AI provider should be a LangChainChatProvider")
|
||||
def step_impl_ai_provider_is_langchain(context: Any) -> None:
|
||||
assert isinstance(context.ai_provider, LangChainChatProvider)
|
||||
|
||||
|
||||
@then("the provider registry AI provider should be a GoogleChatProvider")
|
||||
def step_impl_ai_provider_is_google(context: Any) -> None:
|
||||
assert isinstance(context.ai_provider, GoogleChatProvider)
|
||||
|
||||
|
||||
@then('the provider registry AI provider name should be "{value}"')
|
||||
def step_impl_ai_provider_name(context: Any, value: str) -> None:
|
||||
assert context.ai_provider.name == value
|
||||
|
||||
@@ -1693,6 +1693,12 @@ Notes: Record provider-specific nuances, API limitations, and testing fixtures.
|
||||
- Authored `features/anthropic_provider.feature:1-19` with matching steps in `features/steps/anthropic_provider_steps.py:1-129` to cover missing-key errors, constructor overrides, and metadata reporting while asserting `PlanGenerationGraph` receives the mocked `ChatAnthropic` instance.
|
||||
- Executed `behave features/anthropic_provider.feature` locally to confirm both Anthropic scenarios pass alongside the refreshed OpenAI suite.
|
||||
|
||||
**2025-12-09: Google provider adapter + registry coverage**
|
||||
- Added `src/cleveragents/providers/llm/google_provider.py:1-40` implementing `GoogleChatProvider`, including API key validation, overridable kwargs, and a LangChain `ChatGoogleGenerativeAI` factory wired for streaming.
|
||||
- Updated `src/cleveragents/providers/registry.py:585-614` so `create_ai_provider` now instantiates `GoogleChatProvider` when Google credentials are configured, preserving capability metadata for other providers.
|
||||
- Authored `features/google_provider.feature:1-39` with steps in `features/steps/google_provider_steps.py:1-209` to cover constructor guards, kwarg forwarding, token accounting, and plan-generation graph handoff; expanded `features/provider_registry_coverage.feature:390-422` plus `features/steps/provider_registry_steps.py:12-555` to assert the registry returns the Google adapter instance.
|
||||
- Added Robot coverage via `robot/google_provider.robot:1-169` for generate/stream/error/retry paths, and verified everything with `nox -s unit_tests -- features/google_provider.feature features/provider_registry_coverage.feature` followed by `nox -s integration_tests -- robot/google_provider.robot` (both passing).
|
||||
|
||||
**Additional LangChain/LangGraph Tasks for Phase 4:**
|
||||
- Implement `CodeGenerationGraph` using LangGraph for multi-step code generation
|
||||
- Create `AutoDebugGraph` with conditional edges for error detection and retry
|
||||
@@ -4198,7 +4204,7 @@ If you can do all of the above by end of Day 1, you're on track!
|
||||
- [X] Replace the placeholder `generate_changes()` with the real LangChain-powered workflow by invoking `LangChainChatProvider.generate_changes`, ensuring PlanGenerationGraph produces structured plan deltas and token estimates.
|
||||
- [X] Add streaming support that forwards `ChatOpenAI.stream()` events to PlanService/CLI consumers, including progress callbacks and graceful cancellation semantics (implemented via `LangChainChatProvider.stream_changes()` and consumed by `PlanService.generate_plan_streaming`).
|
||||
- [X] Capture token usage + cost metrics from the `LLMResult` and persist them on `ProviderResponse.token_count`, logging through structlog with LangSmith tags (see `_resolve_token_count/_log_usage` + new plan-service persistence path).
|
||||
- [ ] Expand Behave + Robot coverage to exercise real OpenAI provider flows (using patched clients) covering success, streaming, error, and retry scenarios.
|
||||
- [X] Expand Behave + Robot coverage to exercise real OpenAI provider flows (using patched clients) covering success, streaming, error, and retry scenarios.
|
||||
- [X] Implement Anthropic chat provider adapter (Claude) with parity tests (`src/cleveragents/providers/llm/anthropic_provider.py:1-36`).
|
||||
- [ ] Implement Google Gemini provider adapter with parity tests.
|
||||
- [ ] Implement OpenRouter provider adapter with org header support.
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
*** Settings ***
|
||||
Resource common.resource
|
||||
Library Process
|
||||
Library OperatingSystem
|
||||
Library Collections
|
||||
|
||||
*** Variables ***
|
||||
${PYTHON} python
|
||||
${SRC_DIR} ${CURDIR}/..
|
||||
|
||||
*** Test Cases ***
|
||||
Google Provider Generates Changes
|
||||
${script}= Catenate SEPARATOR=\n
|
||||
... import sys
|
||||
... from unittest.mock import MagicMock, patch
|
||||
... sys.path.insert(0, '${SRC_DIR}/src')
|
||||
... from cleveragents.providers.llm.google_provider import GoogleChatProvider
|
||||
... project = MagicMock(name='Project')
|
||||
... plan = MagicMock(name='Plan')
|
||||
... plan.prompt = 'Add logging'
|
||||
... contexts = [MagicMock(name='Context')]
|
||||
... contexts[0].content = 'ctx'
|
||||
... chat_patcher = patch('cleveragents.providers.llm.google_provider.ChatGoogleGenerativeAI')
|
||||
... graph_patcher = patch('cleveragents.providers.llm.langchain_chat_provider.PlanGenerationGraph')
|
||||
... chat_cls = chat_patcher.start()
|
||||
... graph_cls = graph_patcher.start()
|
||||
... chat_instance = MagicMock(name='ChatGoogleGenerativeAIInstance')
|
||||
... chat_instance.get_num_tokens.return_value = 64
|
||||
... chat_cls.return_value = chat_instance
|
||||
... change = {'plan_id': 1, 'file_path': 'robot_google.py', 'operation': 'modify', 'new_content': '# code'}
|
||||
... graph_instance = MagicMock(name='GraphInstance')
|
||||
... graph_instance.invoke.return_value = {'generated_changes': [change], 'validation_result': {'status': 'PASS'}, 'error': None}
|
||||
... graph_cls.return_value = graph_instance
|
||||
... provider = GoogleChatProvider(api_key='sk-google-robot', model='gemini-2.0-flash')
|
||||
... response = provider.generate_changes(project, plan, contexts)
|
||||
... chat_patcher.stop()
|
||||
... graph_patcher.stop()
|
||||
... assert len(response.changes) == 1
|
||||
... assert response.changes[0].file_path == 'robot_google.py'
|
||||
... assert response.token_count == 64
|
||||
... print('google-provider-success')
|
||||
${result}= Run Process ${PYTHON} -c ${script}
|
||||
Log Process Failure ${result}
|
||||
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} google-provider-success
|
||||
|
||||
Google Provider Streams Events
|
||||
${script}= Catenate SEPARATOR=\n
|
||||
... import sys
|
||||
... from unittest.mock import MagicMock, patch
|
||||
... sys.path.insert(0, '${SRC_DIR}/src')
|
||||
... from cleveragents.providers.llm.google_provider import GoogleChatProvider
|
||||
... project = MagicMock(name='Project')
|
||||
... plan = MagicMock(name='Plan')
|
||||
... plan.prompt = 'Stream plan'
|
||||
... contexts = [MagicMock(name='Context')]
|
||||
... contexts[0].content = 'ctx'
|
||||
... chat_patcher = patch('cleveragents.providers.llm.google_provider.ChatGoogleGenerativeAI')
|
||||
... graph_patcher = patch('cleveragents.providers.llm.langchain_chat_provider.PlanGenerationGraph')
|
||||
... chat_cls = chat_patcher.start()
|
||||
... graph_cls = graph_patcher.start()
|
||||
... chat_instance = MagicMock(name='ChatGoogleGenerativeAIInstance')
|
||||
... chat_instance.get_num_tokens.return_value = 32
|
||||
... chat_cls.return_value = chat_instance
|
||||
... graph_instance = MagicMock(name='GraphInstance')
|
||||
... events = [
|
||||
... {'load_context': {'status': 'ok'}},
|
||||
... {'generate_plan': {'generated_changes': [{'plan_id': 1, 'file_path': 'stream_google.py', 'operation': 'modify', 'new_content': '# stream'}]}},
|
||||
... {'validate': {'validation_result': {'status': 'PASS'}}},
|
||||
... ]
|
||||
... graph_instance.stream.side_effect = lambda *_args, **_kwargs: iter(events)
|
||||
... graph_instance.invoke.return_value = {'generated_changes': [], 'validation_result': {'status': 'PASS'}, 'error': None}
|
||||
... graph_cls.return_value = graph_instance
|
||||
... provider = GoogleChatProvider(api_key='sk-google-robot', model='gemini-2.0-flash')
|
||||
... streamed = list(provider.stream_changes(project, plan, contexts))
|
||||
... chat_patcher.stop()
|
||||
... graph_patcher.stop()
|
||||
... assert streamed[-1]['__end__']['response'].model_used == 'gemini-2.0-flash'
|
||||
... assert len(streamed[-1]['__end__']['response'].changes) == 1
|
||||
... print('google-stream-success')
|
||||
${result}= Run Process ${PYTHON} -c ${script}
|
||||
Log Process Failure ${result}
|
||||
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} google-stream-success
|
||||
|
||||
Google Provider Surfaces Graph Errors
|
||||
${script}= Catenate SEPARATOR=\n
|
||||
... import sys
|
||||
... from unittest.mock import MagicMock, patch
|
||||
... sys.path.insert(0, '${SRC_DIR}/src')
|
||||
... from cleveragents.providers.llm.google_provider import GoogleChatProvider
|
||||
... project = MagicMock(name='Project')
|
||||
... plan = MagicMock(name='Plan')
|
||||
... plan.prompt = 'Error plan'
|
||||
... contexts = [MagicMock(name='Context')]
|
||||
... contexts[0].content = 'ctx'
|
||||
... chat_patcher = patch('cleveragents.providers.llm.google_provider.ChatGoogleGenerativeAI')
|
||||
... graph_patcher = patch('cleveragents.providers.llm.langchain_chat_provider.PlanGenerationGraph')
|
||||
... chat_cls = chat_patcher.start()
|
||||
... graph_cls = graph_patcher.start()
|
||||
... chat_instance = MagicMock(name='ChatGoogleGenerativeAIInstance')
|
||||
... chat_instance.get_num_tokens.return_value = 0
|
||||
... chat_cls.return_value = chat_instance
|
||||
... graph_instance = MagicMock(name='GraphInstance')
|
||||
... graph_instance.invoke.side_effect = ValueError('graph exploded')
|
||||
... graph_cls.return_value = graph_instance
|
||||
... provider = GoogleChatProvider(api_key='sk-google-robot', model='gemini-2.0-flash')
|
||||
... response = provider.generate_changes(project, plan, contexts)
|
||||
... chat_patcher.stop()
|
||||
... graph_patcher.stop()
|
||||
... assert response.error_message == 'graph exploded'
|
||||
... assert response.changes == []
|
||||
... print('google-provider-error')
|
||||
${result}= Run Process ${PYTHON} -c ${script}
|
||||
Log Process Failure ${result}
|
||||
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} google-provider-error
|
||||
|
||||
Google Provider Reports Retry Exhaustion
|
||||
${script}= Catenate SEPARATOR=\n
|
||||
... import sys
|
||||
... from unittest.mock import MagicMock, patch
|
||||
... from tenacity import RetryError, Future
|
||||
... sys.path.insert(0, '${SRC_DIR}/src')
|
||||
... from cleveragents.providers.llm.google_provider import GoogleChatProvider
|
||||
... project = MagicMock(name='Project')
|
||||
... plan = MagicMock(name='Plan')
|
||||
... plan.prompt = 'Retry plan'
|
||||
... contexts = [MagicMock(name='Context')]
|
||||
... contexts[0].content = 'ctx'
|
||||
... future = Future(1)
|
||||
... future.set_exception(RuntimeError('deadline reached'))
|
||||
... retry_error = RetryError(future)
|
||||
... chat_patcher = patch('cleveragents.providers.llm.google_provider.ChatGoogleGenerativeAI')
|
||||
... graph_patcher = patch('cleveragents.providers.llm.langchain_chat_provider.PlanGenerationGraph')
|
||||
... invoke_patcher = patch('cleveragents.providers.llm.langchain_chat_provider.LangChainChatProvider._invoke_with_retry', side_effect=retry_error)
|
||||
... chat_cls = chat_patcher.start()
|
||||
... graph_cls = graph_patcher.start()
|
||||
... invoke_patcher.start()
|
||||
... chat_instance = MagicMock(name='ChatGoogleGenerativeAIInstance')
|
||||
... chat_instance.get_num_tokens.return_value = 0
|
||||
... chat_cls.return_value = chat_instance
|
||||
... graph_instance = MagicMock(name='GraphInstance')
|
||||
... graph_cls.return_value = graph_instance
|
||||
... provider = GoogleChatProvider(api_key='sk-google-robot', model='gemini-2.0-flash')
|
||||
... response = provider.generate_changes(project, plan, contexts)
|
||||
... chat_patcher.stop()
|
||||
... graph_patcher.stop()
|
||||
... invoke_patcher.stop()
|
||||
... assert response.error_message == 'deadline reached'
|
||||
... assert response.changes == []
|
||||
... print('google-provider-retry-error')
|
||||
${result}= Run Process ${PYTHON} -c ${script}
|
||||
Log Process Failure ${result}
|
||||
|
||||
Should Be Equal As Integers ${result.rc} 0
|
||||
Should Contain ${result.stdout} google-provider-retry-error
|
||||
|
||||
*** Keywords ***
|
||||
Log Process Failure
|
||||
[Arguments] ${result}
|
||||
Run Keyword If ${result.rc} == 0 Return From Keyword
|
||||
Log To Console Process failed with rc=${result.rc}
|
||||
Log To Console STDOUT:${\n}${result.stdout}
|
||||
Log To Console STDERR:${\n}${result.stderr}
|
||||
@@ -0,0 +1,39 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from langchain_google_genai import ChatGoogleGenerativeAI
|
||||
|
||||
from cleveragents.providers.llm.langchain_chat_provider import LangChainChatProvider
|
||||
|
||||
|
||||
class GoogleChatProvider(LangChainChatProvider):
|
||||
"""Concrete provider adapter for Google Gemini chat models."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
api_key: str,
|
||||
model: str = "gemini-2.0-flash",
|
||||
max_retries: int = 3,
|
||||
**llm_kwargs: Any,
|
||||
) -> None:
|
||||
if not api_key:
|
||||
raise ValueError("Google API key is required")
|
||||
|
||||
def factory(resolved_model: str) -> ChatGoogleGenerativeAI:
|
||||
kwargs: dict[str, Any] = {
|
||||
"api_key": api_key,
|
||||
"model": resolved_model,
|
||||
}
|
||||
if llm_kwargs:
|
||||
kwargs.update(llm_kwargs)
|
||||
return ChatGoogleGenerativeAI(**kwargs)
|
||||
|
||||
super().__init__(
|
||||
name="google",
|
||||
model_id=model,
|
||||
llm_factory=factory,
|
||||
max_retries=max_retries,
|
||||
supports_streaming=True,
|
||||
)
|
||||
@@ -582,6 +582,27 @@ class ProviderRegistry:
|
||||
provider_info.capabilities if provider_info else ProviderCapabilities()
|
||||
)
|
||||
|
||||
if provider_type == ProviderType.GOOGLE:
|
||||
from cleveragents.providers.llm.google_provider import GoogleChatProvider
|
||||
|
||||
key_attr = self.PROVIDER_KEY_ATTRS.get(provider_type)
|
||||
api_key = getattr(self._settings, key_attr, None) if key_attr else None
|
||||
if not api_key:
|
||||
missing_env = (
|
||||
key_attr.upper() if key_attr else provider_type.value.upper()
|
||||
)
|
||||
raise ValueError(
|
||||
f"Provider {provider_type.value} is not configured. "
|
||||
f"Please set the {missing_env} environment variable."
|
||||
)
|
||||
|
||||
return GoogleChatProvider(
|
||||
api_key=api_key,
|
||||
model=model_id
|
||||
or self.DEFAULT_MODELS.get(provider_type, "gemini-2.0-flash"),
|
||||
max_retries=max_retries,
|
||||
)
|
||||
|
||||
# Create factory function for the LLM
|
||||
def llm_factory(mid: str) -> BaseLanguageModel:
|
||||
return self._create_provider_llm(provider_type, mid) # type: ignore[arg-type]
|
||||
|
||||
Reference in New Issue
Block a user