279 lines
9.4 KiB
Python
279 lines
9.4 KiB
Python
"""Mock AI provider for testing purposes only.
|
|
|
|
This mock implementation is only used during tests, following the mock
|
|
placement rule that all mocks must exist in the features/ directory,
|
|
never in production code (ADR-022).
|
|
"""
|
|
|
|
import os
|
|
from collections.abc import Callable, Iterator
|
|
|
|
from cleveragents.domain.models.core import (
|
|
Change,
|
|
Context,
|
|
OperationType,
|
|
Plan,
|
|
Project,
|
|
)
|
|
from cleveragents.domain.providers.ai_provider import (
|
|
ActorInvocationContext,
|
|
ProviderResponse,
|
|
)
|
|
|
|
|
|
class MockAIProvider:
|
|
"""Mock AI provider that generates simple test responses.
|
|
|
|
This provider is used only during testing to avoid calling real AI APIs.
|
|
"""
|
|
|
|
def __init__(self, model_id: str = "mock-gpt-4"):
|
|
"""Initialize the mock provider.
|
|
|
|
Args:
|
|
model_id: Mock model identifier
|
|
"""
|
|
self._model_id = model_id
|
|
self._name = "MockProvider"
|
|
|
|
def generate_changes(
|
|
self,
|
|
project: Project,
|
|
plan: Plan,
|
|
contexts: list[Context],
|
|
actor_context: ActorInvocationContext | None = None,
|
|
progress_callback: Callable[[int], None] | None = None,
|
|
) -> ProviderResponse:
|
|
"""Generate mock code changes based on the plan.
|
|
|
|
Args:
|
|
project: The project being modified
|
|
plan: The plan containing instructions
|
|
contexts: List of context files to consider
|
|
progress_callback: Optional callback for progress updates (0-100)
|
|
|
|
Returns:
|
|
ProviderResponse containing mock changes
|
|
"""
|
|
# Check if provider should fail (for testing error handling)
|
|
if os.environ.get("CLEVERAGENTS_MOCK_SHOULD_FAIL") == "true":
|
|
return ProviderResponse(
|
|
changes=[],
|
|
model_used=self._model_id,
|
|
token_count=0,
|
|
error_message="Mock provider configured to fail for testing",
|
|
)
|
|
|
|
# Simulate progress
|
|
if progress_callback:
|
|
for i in range(0, 101, 20):
|
|
progress_callback(i)
|
|
|
|
# Generate simple mock changes based on the prompt
|
|
prompt_lower = plan.prompt.lower() if plan.prompt else ""
|
|
|
|
# Check if provider should generate invalid code (for testing validation)
|
|
if os.environ.get("CLEVERAGENTS_MOCK_INVALID_CODE") == "true":
|
|
changes = [
|
|
Change(
|
|
id=None,
|
|
plan_id=plan.id if plan.id else 0,
|
|
file_path="invalid.py",
|
|
operation=OperationType.CREATE,
|
|
original_content=None,
|
|
new_content=(
|
|
"# Invalid Python code\n"
|
|
"def broken_function(\n"
|
|
" # Missing closing parenthesis and colon\n"
|
|
" # This will fail syntax validation\n"
|
|
),
|
|
applied=False,
|
|
applied_at=None,
|
|
new_path=None,
|
|
)
|
|
]
|
|
return ProviderResponse(
|
|
changes=changes,
|
|
model_used=self._model_id,
|
|
token_count=50,
|
|
error_message=None,
|
|
)
|
|
|
|
if not plan.id:
|
|
# Create temporary changes without plan_id for tests
|
|
changes = [
|
|
Change(
|
|
id=None,
|
|
plan_id=0, # Use 0 as placeholder
|
|
file_path="example.py",
|
|
operation=OperationType.CREATE,
|
|
original_content=None,
|
|
new_content="# Mock AI generated code\nprint('Hello from CleverAgents!')\n",
|
|
applied=False,
|
|
applied_at=None,
|
|
new_path=None,
|
|
)
|
|
]
|
|
else:
|
|
# Determine what kind of change to generate based on prompt
|
|
if "error handling" in prompt_lower or "exception" in prompt_lower:
|
|
changes = [
|
|
Change(
|
|
id=None,
|
|
plan_id=plan.id,
|
|
file_path="error_handler.py",
|
|
operation=OperationType.CREATE,
|
|
original_content=None,
|
|
new_content="""# Mock error handling code
|
|
def handle_error(error):
|
|
\"\"\"Mock error handler.\"\"\"
|
|
print(f"Error occurred: {error}")
|
|
return None
|
|
""",
|
|
applied=False,
|
|
applied_at=None,
|
|
new_path=None,
|
|
)
|
|
]
|
|
elif "test" in prompt_lower:
|
|
changes = [
|
|
Change(
|
|
id=None,
|
|
plan_id=plan.id,
|
|
file_path="test_example.py",
|
|
operation=OperationType.CREATE,
|
|
original_content=None,
|
|
new_content="""# Mock test file
|
|
import pytest
|
|
|
|
def test_example():
|
|
\"\"\"Mock test.\"\"\"
|
|
assert True
|
|
""",
|
|
applied=False,
|
|
applied_at=None,
|
|
new_path=None,
|
|
)
|
|
]
|
|
elif "refactor" in prompt_lower or "improve" in prompt_lower:
|
|
# Create a modification change if context files exist
|
|
if contexts and len(contexts) > 0:
|
|
file_path = contexts[0].path
|
|
changes = [
|
|
Change(
|
|
id=None,
|
|
plan_id=plan.id,
|
|
file_path=file_path,
|
|
operation=OperationType.MODIFY,
|
|
original_content=contexts[0].content,
|
|
new_content=(
|
|
f"# Refactored by mock AI\n{contexts[0].content}"
|
|
),
|
|
applied=False,
|
|
applied_at=None,
|
|
new_path=None,
|
|
)
|
|
]
|
|
else:
|
|
# Default to creating a new file
|
|
changes = [
|
|
Change(
|
|
id=None,
|
|
plan_id=plan.id,
|
|
file_path="refactored.py",
|
|
operation=OperationType.CREATE,
|
|
original_content=None,
|
|
new_content=(
|
|
"# Mock refactored code\n"
|
|
"class RefactoredClass:\n"
|
|
" pass\n"
|
|
),
|
|
applied=False,
|
|
applied_at=None,
|
|
new_path=None,
|
|
)
|
|
]
|
|
else:
|
|
# Default change
|
|
changes = [
|
|
Change(
|
|
id=None,
|
|
plan_id=plan.id,
|
|
file_path="example.py",
|
|
operation=OperationType.CREATE,
|
|
original_content=None,
|
|
new_content=(
|
|
"# Mock AI generated code\n"
|
|
"print('Hello from CleverAgents!')\n"
|
|
),
|
|
applied=False,
|
|
applied_at=None,
|
|
new_path=None,
|
|
)
|
|
]
|
|
|
|
return ProviderResponse(
|
|
changes=changes,
|
|
model_used=self._model_id,
|
|
token_count=100,
|
|
error_message=None,
|
|
)
|
|
|
|
def stream_changes(
|
|
self,
|
|
project: Project,
|
|
plan: Plan,
|
|
contexts: list[Context],
|
|
actor_context: ActorInvocationContext | None = None,
|
|
progress_callback: Callable[[int], None] | None = None,
|
|
) -> Iterator[dict[str, object]]:
|
|
"""Stream mock workflow events for CLI coverage."""
|
|
|
|
def _stream() -> Iterator[dict[str, object]]:
|
|
if progress_callback:
|
|
progress_callback(5)
|
|
response = self.generate_changes(
|
|
project,
|
|
plan,
|
|
contexts,
|
|
progress_callback=None,
|
|
)
|
|
for node in (
|
|
"load_context",
|
|
"analyze_requirements",
|
|
"generate_plan",
|
|
"validate",
|
|
):
|
|
event_payload: dict[str, object] = {}
|
|
if node == "generate_plan":
|
|
event_payload = {
|
|
"status": "completed",
|
|
"change_count": len(response.changes),
|
|
}
|
|
elif node == "validate":
|
|
event_payload = {"status": "completed"}
|
|
yield {node: event_payload}
|
|
yield {"__end__": {"response": response}}
|
|
|
|
return _stream()
|
|
|
|
@property
|
|
def name(self) -> str:
|
|
"""Get the provider name."""
|
|
return self._name
|
|
|
|
@name.setter
|
|
def name(self, value: str) -> None:
|
|
"""Allow overriding the provider name in tests."""
|
|
self._name = value
|
|
|
|
@property
|
|
def model_id(self) -> str:
|
|
"""Get the model identifier."""
|
|
return self._model_id
|
|
|
|
@model_id.setter
|
|
def model_id(self, value: str) -> None:
|
|
"""Allow overriding the model identifier in tests."""
|
|
self._model_id = value
|