From f82d9552d16db5cfd2e2d129df4155181148cade Mon Sep 17 00:00:00 2001 From: Jeffrey Phillips Freeman Date: Mon, 8 Dec 2025 23:02:29 -0500 Subject: [PATCH] Feat: Added OpenAI and Antropic provider support and tests --- features/anthropic_provider.feature | 19 +++ features/openai_provider.feature | 22 ++- features/steps/anthropic_provider_steps.py | 129 ++++++++++++++++++ features/steps/openai_provider_steps.py | 124 ++++++++++++++++- implementation_plan.md | 19 ++- src/cleveragents/cli/commands/plan.py | 10 +- .../providers/llm/anthropic_provider.py | 36 +++++ .../providers/llm/openai_provider.py | 67 +++++---- src/cleveragents/providers/registry.py | 3 +- 9 files changed, 379 insertions(+), 50 deletions(-) create mode 100644 features/anthropic_provider.feature create mode 100644 features/steps/anthropic_provider_steps.py create mode 100644 src/cleveragents/providers/llm/anthropic_provider.py diff --git a/features/anthropic_provider.feature b/features/anthropic_provider.feature new file mode 100644 index 000000000..26ef867ff --- /dev/null +++ b/features/anthropic_provider.feature @@ -0,0 +1,19 @@ +Feature: Anthropic chat provider coverage + As a maintainer ensuring provider adapters stay validated + I want Behave scenarios for the Anthropic chat provider + So that the constructor guardrails and keyword handling stay covered + + @unit @providers @anthropic + Scenario: Anthropic provider rejects missing API key + Given I have sample provider domain inputs + When I attempt to create an Anthropic chat provider without an API key + Then the Anthropic provider creation should fail with "Anthropic API key is required" + + @unit @providers @anthropic + Scenario: Anthropic provider forwards keyword overrides to ChatAnthropic + Given I have sample provider domain inputs + When I create an Anthropic chat provider with API key "sk-anthropic-unit" and model "claude-3-5-sonnet-20241022" with custom overrides + And I request plan generation from the Anthropic provider + Then the Anthropic provider should construct ChatAnthropic with api key "sk-anthropic-unit" and model "claude-3-5-sonnet-20241022" + And the Anthropic provider should apply the custom overrides to ChatAnthropic + And the Anthropic provider metadata should report name "anthropic" and model "claude-3-5-sonnet-20241022" diff --git a/features/openai_provider.feature b/features/openai_provider.feature index a738d37d6..984133892 100644 --- a/features/openai_provider.feature +++ b/features/openai_provider.feature @@ -7,13 +7,31 @@ Feature: OpenAI chat provider coverage Scenario: OpenAI provider instantiates ChatOpenAI with provided credentials Given I have sample provider domain inputs When I create an OpenAI chat provider with API key "sk-unit-test" and model "gpt-4o-mini" + And I request plan generation from the OpenAI provider Then the OpenAI provider should construct ChatOpenAI with api key "sk-unit-test" and model "gpt-4o-mini" And the OpenAI provider metadata should report name "openai" and model "gpt-4o-mini" @unit @providers @openai - Scenario: OpenAI provider placeholder response returns no changes + Scenario: OpenAI provider stubbed response reports metadata Given I have sample provider domain inputs And I create an OpenAI chat provider with API key "sk-unit-test" and model "gpt-4o-mini" - When I request placeholder changes from the OpenAI provider + When I request plan generation from the OpenAI provider Then the OpenAI provider response should contain no generated changes And the OpenAI provider response should report the requested model without errors + + @unit @providers @openai + Scenario: OpenAI provider rejects missing API key + Given I have sample provider domain inputs + When I attempt to create an OpenAI chat provider without an API key + Then the OpenAI provider creation should fail with error "OpenAI API key is required" + + @unit @providers @openai + Scenario: OpenAI provider forwards optional organization and kwargs + Given I have sample provider domain inputs + And I set the OpenAI provider organization "org-test-123" + And I set the OpenAI provider extra kwargs "temperature=0.35,max_tokens=256" + When I create an OpenAI chat provider with API key "sk-unit-test" and model "gpt-4o-mini" + And I request plan generation from the OpenAI provider + Then the OpenAI provider should construct ChatOpenAI with api key "sk-unit-test" and model "gpt-4o-mini" + And the OpenAI provider should include organization "org-test-123" in the ChatOpenAI call + And the OpenAI provider should include kwargs "temperature=0.35,max_tokens=256" in the ChatOpenAI call diff --git a/features/steps/anthropic_provider_steps.py b/features/steps/anthropic_provider_steps.py new file mode 100644 index 000000000..394bf73b3 --- /dev/null +++ b/features/steps/anthropic_provider_steps.py @@ -0,0 +1,129 @@ +from __future__ import annotations + +from unittest.mock import MagicMock, patch + +from behave import then, when + +from cleveragents.providers.llm.anthropic_provider import AnthropicChatProvider + + +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 + + +@when("I attempt to create an Anthropic chat provider without an API key") +def step_attempt_create_without_key(context): + context.anthropic_creation_error = None + try: + AnthropicChatProvider(api_key="", model="claude-3-opus-20240229") + except Exception as exc: # pragma: no cover - defensive test guard + context.anthropic_creation_error = exc + + +@then('the Anthropic provider creation should fail with "{error_message}"') +def step_assert_creation_failure(context, error_message): + error = getattr(context, "anthropic_creation_error", None) + assert error is not None, "Anthropic provider creation should have failed" + assert isinstance(error, ValueError) + assert str(error) == error_message + + +@when( + 'I create an Anthropic chat provider with API key "{api_key}" and model "{model}" with custom overrides' +) +def step_create_anthropic_provider_with_overrides(context, api_key, model): + patcher = patch("cleveragents.providers.llm.anthropic_provider.ChatAnthropic") + context.chat_anthropic_patcher = patcher + mock_chat_class = patcher.start() + _register_cleanup(context, patcher.stop) + mock_chat_instance = MagicMock(name="ChatAnthropicInstance") + mock_chat_instance.get_num_tokens = MagicMock(return_value=0) + mock_chat_class.return_value = mock_chat_instance + + context.anthropic_overrides = { + "temperature": 0.15, + "max_tokens": 2048, + "default_request_timeout": 12, + } + + context.provider = AnthropicChatProvider( + api_key=api_key, + model=model, + max_retries=2, + **context.anthropic_overrides, + ) + context.chat_anthropic_class = mock_chat_class + context.chat_anthropic_instance = mock_chat_instance + + +@when("I request plan generation from the Anthropic provider") +def step_request_plan_generation_anthropic(context): + 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") + mock_graph_instance.invoke.return_value = { + "generated_changes": [], + "validation_result": {"status": "PASS"}, + "error": None, + } + mock_graph_instance.stream.return_value = iter(()) + mock_graph_class.return_value = mock_graph_instance + + context.plan_generation_graph = mock_graph_instance + context.plan_generation_graph_class = mock_graph_class + + context.response = context.provider.generate_changes( + context.project, + context.plan, + context.contexts, + ) + context.chat_anthropic_call = context.chat_anthropic_class.call_args + context.plan_generation_graph_call = mock_graph_class.call_args + + +@then( + 'the Anthropic provider should construct ChatAnthropic with api key "{api_key}" and model "{model}"' +) +def step_assert_chat_anthropic_construction(context, api_key, model): + call = getattr(context, "chat_anthropic_call", None) + assert call is not None, "ChatAnthropic should have been constructed" + call_args, call_kwargs = call + assert call_args == (), "ChatAnthropic should be called with keyword arguments" + assert call_kwargs["api_key"] == api_key + assert call_kwargs["model"] == model + + graph_call = getattr(context, "plan_generation_graph_call", None) + assert graph_call is not None, "PlanGenerationGraph should be constructed" + _, graph_kwargs = graph_call + assert graph_kwargs["llm"] is context.chat_anthropic_instance + + +@then("the Anthropic provider should apply the custom overrides to ChatAnthropic") +def step_assert_anthropic_overrides(context): + call = getattr(context, "chat_anthropic_call", None) + assert call is not None, "ChatAnthropic should have been constructed" + _, call_kwargs = call + overrides = getattr(context, "anthropic_overrides", None) + assert overrides, "Custom overrides should have been captured" + + for key, value in overrides.items(): + assert call_kwargs.get(key) == value, f"Override {key} was not forwarded" + + +@then( + 'the Anthropic provider metadata should report name "{expected_name}" and model "{expected_model}"' +) +def step_assert_anthropic_metadata(context, expected_name, expected_model): + provider = getattr(context, "provider", None) + assert provider is not None, "Anthropic provider should exist in context" + assert provider.name == expected_name + assert provider.model_id == expected_model diff --git a/features/steps/openai_provider_steps.py b/features/steps/openai_provider_steps.py index 5baf3fa71..52e6e5085 100644 --- a/features/steps/openai_provider_steps.py +++ b/features/steps/openai_provider_steps.py @@ -1,5 +1,7 @@ from __future__ import annotations +import ast +from typing import Any from unittest.mock import MagicMock, patch from behave import given, then, when @@ -17,6 +19,28 @@ def _register_cleanup(context, cleanup): context._cleanup_handlers = cleanup_handlers +def _parse_kwargs_string(kwargs_string: str) -> dict[str, Any]: + if not kwargs_string: + return {} + result: dict[str, Any] = {} + for entry in kwargs_string.split(","): + entry = entry.strip() + if not entry: + continue + if "=" not in entry: + result[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 + result[key] = parsed_value + return result + + @given("I have sample provider domain inputs") def step_sample_provider_inputs(context): context.project = MagicMock(spec=Project) @@ -26,6 +50,19 @@ def step_sample_provider_inputs(context): context.contexts[0].content = "Sample context entry" +@given('I set the OpenAI provider organization "{organization}"') +def step_set_openai_provider_org(context, organization): + if organization.lower() == "none": + context.openai_provider_org = None + else: + context.openai_provider_org = organization + + +@given('I set the OpenAI provider extra kwargs "{kwargs_string}"') +def step_set_openai_provider_kwargs(context, kwargs_string): + context.openai_provider_kwargs = _parse_kwargs_string(kwargs_string) + + @given( 'I create an OpenAI chat provider with API key "{api_key}" and model "{model_id}"' ) @@ -38,21 +75,57 @@ def step_create_openai_provider(context, api_key, model_id): mock_chat_openai_class = patcher.start() _register_cleanup(context, patcher.stop) mock_chat_openai_instance = MagicMock(name="ChatOpenAIInstance") + mock_chat_openai_instance.get_num_tokens = MagicMock(return_value=0) mock_chat_openai_class.return_value = mock_chat_openai_instance - context.provider = OpenAIChatProvider(api_key=api_key, model=model_id) - context.chat_openai_call = mock_chat_openai_class.call_args + organization = getattr(context, "openai_provider_org", None) + extra_kwargs = dict(getattr(context, "openai_provider_kwargs", {})) + + context.provider = OpenAIChatProvider( + api_key=api_key, + model=model_id, + organization=organization, + **extra_kwargs, + ) + context.chat_openai_class = mock_chat_openai_class context.chat_openai_instance = mock_chat_openai_instance - context.requested_credentials = {"api_key": api_key, "model": model_id} -@when("I request placeholder changes from the OpenAI provider") -def step_request_placeholder_changes(context): +@when("I attempt to create an OpenAI chat provider without an API key") +def step_openai_provider_without_api_key(context): + context.openai_provider_error = None + try: + OpenAIChatProvider(api_key="", model="gpt-4o-mini") + except Exception as exc: # pragma: no cover - defensive logging only + context.openai_provider_error = exc + + +@when("I request plan generation from the OpenAI provider") +def step_request_plan_generation(context): + 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") + mock_graph_instance.invoke.return_value = { + "generated_changes": [], + "validation_result": {"status": "PASS"}, + "error": None, + } + mock_graph_instance.stream.return_value = iter(()) + mock_graph_class.return_value = mock_graph_instance + context.plan_generation_graph = mock_graph_instance + context.plan_generation_graph_class = mock_graph_class + context.response = context.provider.generate_changes( context.project, context.plan, context.contexts, ) + context.chat_openai_call = context.chat_openai_class.call_args + context.plan_generation_graph_call = mock_graph_class.call_args @then( @@ -64,7 +137,38 @@ def step_assert_chat_openai_constructor(context, api_key, model_id): assert call_args == (), "ChatOpenAI should be called with keyword arguments" assert call_kwargs["api_key"] == api_key assert call_kwargs["model"] == model_id - assert context.provider.llm is context.chat_openai_instance + + assert context.plan_generation_graph_call is not None, ( + "PlanGenerationGraph should receive the ChatOpenAI instance" + ) + _, graph_kwargs = context.plan_generation_graph_call + assert graph_kwargs["llm"] is context.chat_openai_instance + + +@then( + 'the OpenAI provider should include organization "{expected_organization}" in the ChatOpenAI call' +) +def step_assert_chat_openai_org(context, expected_organization): + assert context.chat_openai_call is not None, "ChatOpenAI should have been called" + _, call_kwargs = context.chat_openai_call + normalized = ( + None if expected_organization.lower() == "none" else expected_organization + ) + assert call_kwargs.get("organization") == normalized + + +@then( + 'the OpenAI provider should include kwargs "{kwargs_string}" in the ChatOpenAI call' +) +def step_assert_chat_openai_kwargs(context, kwargs_string): + assert context.chat_openai_call is not None, "ChatOpenAI should have been called" + _, call_kwargs = context.chat_openai_call + expected_kwargs = _parse_kwargs_string(kwargs_string) + for key, value in expected_kwargs.items(): + assert key in call_kwargs, f"Expected {key} in ChatOpenAI kwargs" + assert call_kwargs[key] == value, ( + f"Expected ChatOpenAI kwargs[{key!r}] to equal {value!r}" + ) @then( @@ -87,3 +191,11 @@ def step_assert_placeholder_metadata(context): assert context.response is not None assert context.response.model_used == context.provider.model_id assert context.response.error_message in (None, "") + + +@then('the OpenAI provider creation should fail with error "{message}"') +def step_assert_openai_provider_error(context, message): + error = getattr(context, "openai_provider_error", None) + assert error is not None, "Expected the provider to raise an error" + assert isinstance(error, ValueError) + assert str(error) == message diff --git a/implementation_plan.md b/implementation_plan.md index 9e4af5b28..fc7d161f5 100644 --- a/implementation_plan.md +++ b/implementation_plan.md @@ -1672,6 +1672,16 @@ Notes: Record provider-specific nuances, API limitations, and testing fixtures. - `features/openai_provider.feature:7-22` now runs end-to-end against the real provider adapter (with LangChain mocked), giving regression coverage for constructor wiring and placeholder outputs until real streaming paths land. - Executed `behave features/openai_provider.feature` locally; both scenarios pass and assert no generated changes while reporting the requested model, preventing regressions in provider bootstrap logic. +**2025-12-09: OpenAI provider LangGraph stub coverage refresh** +- Updated `features/openai_provider.feature:7-31` to exercise the `LangChainChatProvider.generate_changes` path so ChatOpenAI instantiation and LangGraph invocation occur under test. +- Step definitions in `features/steps/openai_provider_steps.py:17-191` now patch `PlanGenerationGraph` directly, assert organization/kwargs forwarding, and guarantee deterministic responses for CI coverage. +- Re-ran `behave features/openai_provider.feature` to document the green run for this refreshed coverage and keep the LangGraph/provider integration verified in CI logs. + +**2025-12-09: Anthropic provider adapter + coverage** +- Added `src/cleveragents/providers/llm/anthropic_provider.py:1-36` implementing `AnthropicChatProvider`, a LangChain-backed adapter that validates API keys, forwards override kwargs, and streams progress through `LangChainChatProvider`. +- 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. + **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 @@ -4173,12 +4183,12 @@ If you can do all of the above by end of Day 1, you're on track! - [ ] Code: Provider registry and runtime integration (Phase 4 scope) - [x] Implement `ProviderRegistry` under `src/cleveragents/providers/registry.py` that discovers configured providers from `Settings`, reports capability metadata, and selects defaults based on `CLEVERAGENTS_DEFAULT_PROVIDER` / `CLEVERAGENTS_DEFAULT_MODEL`. - [ ] Create concrete provider adapters in `src/cleveragents/providers/llm/` (OpenAI, Anthropic, Google, OpenRouter at minimum) wrapping LangChain chat models behind `AIProviderInterface`, including streaming hooks, retry annotations, and token accounting. - - [X] Implement `OpenAIChatProvider` placeholder in `src/cleveragents/providers/llm/openai_provider.py:1` wrapping `ChatOpenAI` with API key/model inputs and returning metadata-only responses until real streaming lands. Behave coverage lives in `features/openai_provider.feature:7-22` with steps under `features/steps/openai_provider_steps.py:20-89`. - - [ ] Replace placeholder `generate_changes()` with a real LangChain-powered runnable that invokes `ChatOpenAI` (or the selected provider) to produce structured plan deltas, maps them into `ProviderResponse.changes`, and surfaces model tokens/errors via LangSmith metadata. + - [X] Implement `OpenAIChatProvider` in `src/cleveragents/providers/llm/openai_provider.py:1-60` as a LangChain-backed adapter that validates API keys, forwards organization/kwargs overrides, and delegates execution to `LangChainChatProvider`. + - [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. - [ ] Add streaming support that forwards `ChatOpenAI.stream()` events to PlanService/CLI consumers, including progress callbacks and graceful cancellation semantics. - [ ] Capture token usage + cost metrics from the `LLMResult` and persist them on `ProviderResponse.token_count`, logging through structlog with LangSmith tags. - [ ] Expand Behave + Robot coverage to exercise real OpenAI provider flows (using patched clients) covering success, streaming, error, and retry scenarios. - - [ ] Implement Anthropic chat provider adapter (Claude) with parity tests. + - [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. - [ ] Extend `Settings` with provider/model defaults, per-provider configuration (Azure deployments, OpenRouter org, etc.), and validation so missing env vars generate actionable errors. @@ -4193,7 +4203,8 @@ If you can do all of the above by end of Day 1, you're on track! - [ ] Extend ADR-008 (or add an addendum) capturing the registry implementation details, override order (CLI → env → defaults), and how new providers plug into the system. - [ ] Tests: Provider selection and CLI override coverage - [x] Create a dedicated Behave suite (`features/provider_registry_coverage.feature` + `features/steps/provider_registry_steps.py`) that simulates different env configurations, verifies registry discovery, default selection, CLI overrides, and failure messaging. - - [X] Add targeted OpenAI provider placeholder coverage in `features/openai_provider.feature:7-22` with steps `features/steps/openai_provider_steps.py:20-89` to assert ChatOpenAI constructor wiring and metadata-only responses until full provider integration completes. + - [X] Add LangGraph-backed OpenAI provider coverage in `features/openai_provider.feature:7-31` with steps `features/steps/openai_provider_steps.py:17-191` to assert constructor wiring, kwargs/organization overrides, missing-key errors, and PlanGenerationGraph invocation. + - [X] Add Anthropic provider coverage in `features/anthropic_provider.feature:1-19` with steps `features/steps/anthropic_provider_steps.py:1-129` validating API key enforcement, override forwarding, and metadata reporting. - [X] Add Behave scenarios to `features/plan_service.feature` ensuring `plan build` succeeds when provider keys are set, fails with actionable guidance when they are missing, and honors `--provider/--model` flags. (COMPLETED 2025-12-08) - [ ] Add Robot integration tests (e.g., `robot/provider_registry.robot`) that exercise real CLI flows with env overrides, confirm DI wiring returns the expected provider type, and capture logs showing selected providers. - [ ] Update existing streaming/auto-debug Behave suites to cover mixed-provider scenarios (e.g., build with OpenAI, auto-debug fallback to Anthropic) and ensure token accounting + metadata propagate into LangGraph state. diff --git a/src/cleveragents/cli/commands/plan.py b/src/cleveragents/cli/commands/plan.py index 666cd478f..bf3010e01 100644 --- a/src/cleveragents/cli/commands/plan.py +++ b/src/cleveragents/cli/commands/plan.py @@ -373,14 +373,20 @@ def tell( str | None, typer.Option( "--provider", - help="Override the default AI provider (takes precedence over CLEVERAGENTS_DEFAULT_PROVIDER)", + help=( + "Override the default AI provider " + "(takes precedence over CLEVERAGENTS_DEFAULT_PROVIDER)" + ), ), ] = None, model: Annotated[ str | None, typer.Option( "--model", - help="Override the default model (takes precedence over CLEVERAGENTS_DEFAULT_MODEL)", + help=( + "Override the default model " + "(takes precedence over CLEVERAGENTS_DEFAULT_MODEL)" + ), ), ] = None, stream: Annotated[ diff --git a/src/cleveragents/providers/llm/anthropic_provider.py b/src/cleveragents/providers/llm/anthropic_provider.py new file mode 100644 index 000000000..e9aecc45e --- /dev/null +++ b/src/cleveragents/providers/llm/anthropic_provider.py @@ -0,0 +1,36 @@ +from __future__ import annotations + +from typing import Any + +from langchain_anthropic import ChatAnthropic + +from cleveragents.providers.llm.langchain_chat_provider import LangChainChatProvider + + +class AnthropicChatProvider(LangChainChatProvider): + """Concrete provider adapter for Anthropic Claude models.""" + + def __init__( + self, + *, + api_key: str, + model: str = "claude-sonnet-4-20250514", + max_retries: int = 3, + **llm_kwargs: Any, + ) -> None: + if not api_key: + raise ValueError("Anthropic API key is required") + + def factory(resolved_model: str) -> ChatAnthropic: + kwargs: dict[str, Any] = {"model": resolved_model, "api_key": api_key} + if llm_kwargs: + kwargs.update(llm_kwargs) + return ChatAnthropic(**kwargs) + + super().__init__( + name="anthropic", + model_id=model, + llm_factory=factory, + max_retries=max_retries, + supports_streaming=True, + ) diff --git a/src/cleveragents/providers/llm/openai_provider.py b/src/cleveragents/providers/llm/openai_provider.py index b0f37b568..60bb855d4 100644 --- a/src/cleveragents/providers/llm/openai_provider.py +++ b/src/cleveragents/providers/llm/openai_provider.py @@ -1,42 +1,39 @@ -from typing import Callable -from cleveragents.domain.models.core import Context, Plan, Project -from cleveragents.domain.providers.ai_provider import ( - AIProviderInterface, - ProviderResponse, -) +from __future__ import annotations + +from typing import Any + from langchain_openai import ChatOpenAI +from cleveragents.providers.llm.langchain_chat_provider import LangChainChatProvider -class OpenAIChatProvider(AIProviderInterface): - """ - OpenAI-specific chat provider. - """ - def __init__(self, api_key: str, model: str = "gpt-4o"): - self._api_key = api_key - self._model_id = model - self.llm = ChatOpenAI(api_key=api_key, model=model) +class OpenAIChatProvider(LangChainChatProvider): + """Concrete LangChain-backed provider for OpenAI chat models.""" - @property - def name(self) -> str: - return "openai" - - @property - def model_id(self) -> str: - return self._model_id - - def generate_changes( + def __init__( self, - project: Project, - plan: Plan, - contexts: list[Context], - progress_callback: Callable[[int], None] | None = None, - ) -> ProviderResponse: - # This is a placeholder. The actual implementation will be added in a future commit. - # For now, we return an empty response. - return ProviderResponse( - changes=[], - model_used=self.model_id, - token_count=0, - error_message=None, + *, + api_key: str, + model: str = "gpt-4o", + max_retries: int = 3, + organization: str | None = None, + **llm_kwargs: Any, + ) -> None: + if not api_key: + raise ValueError("OpenAI API key is required") + + def factory(resolved_model: str) -> ChatOpenAI: + kwargs: dict[str, Any] = {"api_key": api_key, "model": resolved_model} + if organization: + kwargs["organization"] = organization + if llm_kwargs: + kwargs.update(llm_kwargs) + return ChatOpenAI(**kwargs) + + super().__init__( + name="openai", + model_id=model, + llm_factory=factory, + max_retries=max_retries, + supports_streaming=True, ) diff --git a/src/cleveragents/providers/registry.py b/src/cleveragents/providers/registry.py index 3b6fc2d8a..2997f622c 100644 --- a/src/cleveragents/providers/registry.py +++ b/src/cleveragents/providers/registry.py @@ -465,7 +465,8 @@ class ProviderRegistry: azure_endpoint = endpoint_override or self._settings.azure_openai_endpoint if not azure_endpoint: raise ValueError( - "Azure OpenAI endpoint not configured. Set AZURE_OPENAI_ENDPOINT or CLEVERAGENTS_AZURE_OPENAI_ENDPOINT." + "Azure OpenAI endpoint not configured. " + "Set AZURE_OPENAI_ENDPOINT or CLEVERAGENTS_AZURE_OPENAI_ENDPOINT." ) api_version_override = _coerce_optional_str(