Added comprehensive implementation plan for development mode feature and test configuration used during verification. Files: - docs/plans/2025-11-01-dev-mode-mock-ai.md: Complete 12-task plan - configs/test_dev_mode.json: Test configuration for dev mode These files document the feature implementation process and provide reference configurations for testing.
48 KiB
Development Mode with Mock AI Implementation Plan
For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
Goal: Add DEPLOYMENT_MODE environment variable that disables AI API calls in DEV mode and isolates dev data to separate database and file paths.
Architecture:
- Separate data paths (
data/agent_data/vsdata/dev_agent_data/) and databases (trading.dbvstrading_dev.db) based onDEPLOYMENT_MODE - Mock AI provider returns static but rotating responses (Day 1: AAPL, Day 2: MSFT, Day 3: GOOGL, etc.)
- Dev database reset on startup (unless
PRESERVE_DEV_DATA=true) - Warning logs when production API keys detected in DEV mode
- API responses include
deployment_modefield
Tech Stack: Python 3.10+, LangChain, SQLite, environment variables
Task 1: Update Environment Configuration
Files:
- Modify:
.env.example - Read:
.env.example:1-42
Step 1: Document deployment mode variables
Add the following to .env.example after line 42:
# =============================================================================
# Deployment Mode Configuration
# =============================================================================
# DEPLOYMENT_MODE controls AI model calls and data isolation
# - PROD: Real AI API calls, uses data/agent_data/ and data/trading.db
# - DEV: Mock AI responses, uses data/dev_agent_data/ and data/trading_dev.db
DEPLOYMENT_MODE=PROD
# Preserve dev data between runs (DEV mode only)
# Set to true to keep dev database and files for debugging
PRESERVE_DEV_DATA=false
Step 2: Verify changes
Run: cat .env.example
Expected: New section appears at end of file
Step 3: Commit
git add .env.example
git commit -m "docs: add DEPLOYMENT_MODE configuration to env example"
Task 2: Create Mock AI Provider
Files:
- Create:
agent/mock_provider/mock_ai_provider.py - Create:
agent/mock_provider/__init__.py
Step 1: Write test for mock provider rotation
Create tests/unit/test_mock_provider.py:
import pytest
from agent.mock_provider.mock_ai_provider import MockAIProvider
def test_mock_provider_rotates_stocks():
"""Test that mock provider returns different stocks on different days"""
provider = MockAIProvider()
# Day 1 should recommend AAPL
response1 = provider.generate_response("2025-01-01", step=0)
assert "AAPL" in response1
assert "<FINISH_SIGNAL>" in response1
# Day 2 should recommend MSFT
response2 = provider.generate_response("2025-01-02", step=0)
assert "MSFT" in response2
assert "<FINISH_SIGNAL>" in response2
# Responses should be different
assert response1 != response2
def test_mock_provider_finish_signal():
"""Test that all responses include finish signal"""
provider = MockAIProvider()
response = provider.generate_response("2025-01-01", step=0)
assert "<FINISH_SIGNAL>" in response
def test_mock_provider_valid_json_tool_calls():
"""Test that responses contain valid tool call syntax"""
provider = MockAIProvider()
response = provider.generate_response("2025-01-01", step=0)
assert "[calls tool_get_price" in response or "get_price" in response.lower()
Step 2: Run test to verify it fails
Run: pytest tests/unit/test_mock_provider.py -v
Expected: FAIL with "ModuleNotFoundError: No module named 'agent.mock_provider'"
Step 3: Create mock provider implementation
Create agent/mock_provider/__init__.py:
"""Mock AI provider for development mode testing"""
from .mock_ai_provider import MockAIProvider
__all__ = ["MockAIProvider"]
Create agent/mock_provider/mock_ai_provider.py:
"""
Mock AI Provider for Development Mode
Returns static but rotating trading responses to test orchestration without AI API costs.
Rotates through NASDAQ 100 stocks in a predictable pattern.
"""
from typing import Optional
from datetime import datetime
class MockAIProvider:
"""Mock AI provider that returns pre-defined trading responses"""
# Rotation of stocks for variety in testing
STOCK_ROTATION = [
"AAPL", "MSFT", "GOOGL", "AMZN", "NVDA",
"META", "TSLA", "BRK.B", "UNH", "JNJ"
]
def __init__(self):
"""Initialize mock provider"""
pass
def generate_response(self, date: str, step: int = 0) -> str:
"""
Generate mock trading response based on date
Args:
date: Trading date (YYYY-MM-DD)
step: Current step in reasoning loop (0-indexed)
Returns:
Mock AI response string with tool calls and finish signal
"""
# Use date to deterministically select stock
date_obj = datetime.strptime(date, "%Y-%m-%d")
day_offset = (date_obj - datetime(2025, 1, 1)).days
stock_idx = day_offset % len(self.STOCK_ROTATION)
selected_stock = self.STOCK_ROTATION[stock_idx]
# Generate mock response
response = f"""Let me analyze the market for today ({date}).
I'll check the current price for {selected_stock}.
[calls tool_get_price with symbol={selected_stock}]
Based on the analysis, I'll make a small purchase to test the system.
[calls tool_trade with action=buy, symbol={selected_stock}, amount=5]
I've completed today's trading session.
<FINISH_SIGNAL>"""
return response
def __str__(self):
return "MockAIProvider(mode=development)"
def __repr__(self):
return self.__str__()
Step 4: Run test to verify it passes
Run: pytest tests/unit/test_mock_provider.py -v
Expected: PASS (3 tests)
Step 5: Commit
git add agent/mock_provider/ tests/unit/test_mock_provider.py
git commit -m "feat: add mock AI provider for dev mode with stock rotation"
Task 3: Create Mock LangChain Model Wrapper
Files:
- Create:
agent/mock_provider/mock_langchain_model.py - Modify:
agent/mock_provider/__init__.py
Step 1: Write test for LangChain model wrapper
Add to tests/unit/test_mock_provider.py:
import asyncio
from agent.mock_provider.mock_langchain_model import MockChatModel
def test_mock_chat_model_invoke():
"""Test synchronous invoke returns proper message format"""
model = MockChatModel(date="2025-01-01")
messages = [{"role": "user", "content": "Analyze the market"}]
response = model.invoke(messages)
assert hasattr(response, "content")
assert "AAPL" in response.content
assert "<FINISH_SIGNAL>" in response.content
def test_mock_chat_model_ainvoke():
"""Test asynchronous invoke returns proper message format"""
async def run_test():
model = MockChatModel(date="2025-01-02")
messages = [{"role": "user", "content": "Analyze the market"}]
response = await model.ainvoke(messages)
assert hasattr(response, "content")
assert "MSFT" in response.content
assert "<FINISH_SIGNAL>" in response.content
asyncio.run(run_test())
def test_mock_chat_model_different_dates():
"""Test that different dates produce different responses"""
model1 = MockChatModel(date="2025-01-01")
model2 = MockChatModel(date="2025-01-02")
msg = [{"role": "user", "content": "Trade"}]
response1 = model1.invoke(msg)
response2 = model2.invoke(msg)
assert response1.content != response2.content
Step 2: Run test to verify it fails
Run: pytest tests/unit/test_mock_provider.py::test_mock_chat_model_invoke -v
Expected: FAIL with "ImportError: cannot import name 'MockChatModel'"
Step 3: Implement mock LangChain model
Create agent/mock_provider/mock_langchain_model.py:
"""
Mock LangChain-compatible chat model for development mode
Wraps MockAIProvider to work with LangChain's agent framework.
"""
from typing import Any, List, Optional, Dict
from langchain_core.language_models import BaseChatModel
from langchain_core.messages import AIMessage, BaseMessage
from langchain_core.outputs import ChatResult, ChatGeneration
from .mock_ai_provider import MockAIProvider
class MockChatModel(BaseChatModel):
"""
Mock chat model compatible with LangChain's agent framework
Attributes:
date: Current trading date for response generation
step_counter: Tracks reasoning steps within a trading session
"""
date: str = "2025-01-01"
step_counter: int = 0
def __init__(self, date: str = "2025-01-01", **kwargs):
"""
Initialize mock chat model
Args:
date: Trading date for mock responses
**kwargs: Additional LangChain model parameters
"""
super().__init__(**kwargs)
self.date = date
self.step_counter = 0
self.provider = MockAIProvider()
@property
def _llm_type(self) -> str:
"""Return identifier for this LLM type"""
return "mock-chat-model"
def _generate(
self,
messages: List[BaseMessage],
stop: Optional[List[str]] = None,
run_manager: Optional[Any] = None,
**kwargs: Any,
) -> ChatResult:
"""
Generate mock response (synchronous)
Args:
messages: Input messages (ignored in mock)
stop: Stop sequences (ignored in mock)
run_manager: LangChain run manager
**kwargs: Additional generation parameters
Returns:
ChatResult with mock AI response
"""
response_text = self.provider.generate_response(self.date, self.step_counter)
self.step_counter += 1
message = AIMessage(
content=response_text,
response_metadata={"finish_reason": "stop"}
)
generation = ChatGeneration(message=message)
return ChatResult(generations=[generation])
async def _agenerate(
self,
messages: List[BaseMessage],
stop: Optional[List[str]] = None,
run_manager: Optional[Any] = None,
**kwargs: Any,
) -> ChatResult:
"""
Generate mock response (asynchronous)
Same as _generate but async-compatible for LangChain agents.
"""
return self._generate(messages, stop, run_manager, **kwargs)
def invoke(self, input: Any, **kwargs) -> AIMessage:
"""Synchronous invoke (LangChain compatibility)"""
if isinstance(input, list):
messages = input
else:
messages = []
result = self._generate(messages, **kwargs)
return result.generations[0].message
async def ainvoke(self, input: Any, **kwargs) -> AIMessage:
"""Asynchronous invoke (LangChain compatibility)"""
if isinstance(input, list):
messages = input
else:
messages = []
result = await self._agenerate(messages, **kwargs)
return result.generations[0].message
Update agent/mock_provider/__init__.py:
"""Mock AI provider for development mode testing"""
from .mock_ai_provider import MockAIProvider
from .mock_langchain_model import MockChatModel
__all__ = ["MockAIProvider", "MockChatModel"]
Step 4: Run tests to verify they pass
Run: pytest tests/unit/test_mock_provider.py -v
Expected: PASS (6 tests)
Step 5: Commit
git add agent/mock_provider/mock_langchain_model.py agent/mock_provider/__init__.py tests/unit/test_mock_provider.py
git commit -m "feat: add LangChain-compatible mock chat model wrapper"
Task 4: Add Deployment Mode Configuration Module
Files:
- Create:
tools/deployment_config.py - Modify:
tools/__init__.py
Step 1: Write tests for deployment config
Create tests/unit/test_deployment_config.py:
import os
import pytest
from tools.deployment_config import (
get_deployment_mode,
is_dev_mode,
is_prod_mode,
get_data_path,
get_db_path,
should_preserve_dev_data,
log_api_key_warning
)
def test_get_deployment_mode_default():
"""Test default deployment mode is PROD"""
# Clear env to test default
os.environ.pop("DEPLOYMENT_MODE", None)
assert get_deployment_mode() == "PROD"
def test_get_deployment_mode_dev():
"""Test DEV mode detection"""
os.environ["DEPLOYMENT_MODE"] = "DEV"
assert get_deployment_mode() == "DEV"
assert is_dev_mode() == True
assert is_prod_mode() == False
def test_get_deployment_mode_prod():
"""Test PROD mode detection"""
os.environ["DEPLOYMENT_MODE"] = "PROD"
assert get_deployment_mode() == "PROD"
assert is_dev_mode() == False
assert is_prod_mode() == True
def test_get_data_path_prod():
"""Test production data path"""
os.environ["DEPLOYMENT_MODE"] = "PROD"
assert get_data_path("./data/agent_data") == "./data/agent_data"
def test_get_data_path_dev():
"""Test dev data path substitution"""
os.environ["DEPLOYMENT_MODE"] = "DEV"
assert get_data_path("./data/agent_data") == "./data/dev_agent_data"
def test_get_db_path_prod():
"""Test production database path"""
os.environ["DEPLOYMENT_MODE"] = "PROD"
assert get_db_path("data/trading.db") == "data/trading.db"
def test_get_db_path_dev():
"""Test dev database path substitution"""
os.environ["DEPLOYMENT_MODE"] = "DEV"
assert get_db_path("data/trading.db") == "data/trading_dev.db"
assert get_db_path("data/jobs.db") == "data/jobs_dev.db"
def test_should_preserve_dev_data_default():
"""Test default preserve flag is False"""
os.environ.pop("PRESERVE_DEV_DATA", None)
assert should_preserve_dev_data() == False
def test_should_preserve_dev_data_true():
"""Test preserve flag can be enabled"""
os.environ["PRESERVE_DEV_DATA"] = "true"
assert should_preserve_dev_data() == True
def test_log_api_key_warning_in_dev(capsys):
"""Test warning logged when API keys present in DEV mode"""
os.environ["DEPLOYMENT_MODE"] = "DEV"
os.environ["OPENAI_API_KEY"] = "sk-test123"
log_api_key_warning()
captured = capsys.readouterr()
assert "⚠️ WARNING: Production API keys detected in DEV mode" in captured.out
assert "OPENAI_API_KEY" in captured.out
Step 2: Run test to verify it fails
Run: pytest tests/unit/test_deployment_config.py -v
Expected: FAIL with "ModuleNotFoundError: No module named 'tools.deployment_config'"
Step 3: Implement deployment config module
Create tools/deployment_config.py:
"""
Deployment mode configuration utilities
Handles PROD vs DEV mode differentiation including:
- Data path isolation
- Database path isolation
- API key validation warnings
- Deployment mode detection
"""
import os
from typing import Optional
def get_deployment_mode() -> str:
"""
Get current deployment mode
Returns:
"PROD" or "DEV" (defaults to PROD if not set)
"""
mode = os.getenv("DEPLOYMENT_MODE", "PROD").upper()
if mode not in ["PROD", "DEV"]:
print(f"⚠️ Invalid DEPLOYMENT_MODE '{mode}', defaulting to PROD")
return "PROD"
return mode
def is_dev_mode() -> bool:
"""Check if running in DEV mode"""
return get_deployment_mode() == "DEV"
def is_prod_mode() -> bool:
"""Check if running in PROD mode"""
return get_deployment_mode() == "PROD"
def get_data_path(base_path: str) -> str:
"""
Get data path based on deployment mode
Args:
base_path: Base data path (e.g., "./data/agent_data")
Returns:
Modified path for DEV mode or original for PROD
Example:
PROD: "./data/agent_data" -> "./data/agent_data"
DEV: "./data/agent_data" -> "./data/dev_agent_data"
"""
if is_dev_mode():
# Replace agent_data with dev_agent_data
return base_path.replace("agent_data", "dev_agent_data")
return base_path
def get_db_path(base_db_path: str) -> str:
"""
Get database path based on deployment mode
Args:
base_db_path: Base database path (e.g., "data/trading.db")
Returns:
Modified path for DEV mode or original for PROD
Example:
PROD: "data/trading.db" -> "data/trading.db"
DEV: "data/trading.db" -> "data/trading_dev.db"
"""
if is_dev_mode():
# Insert _dev before .db extension
if base_db_path.endswith(".db"):
return base_db_path[:-3] + "_dev.db"
return base_db_path + "_dev"
return base_db_path
def should_preserve_dev_data() -> bool:
"""
Check if dev data should be preserved between runs
Returns:
True if PRESERVE_DEV_DATA=true, False otherwise
"""
preserve = os.getenv("PRESERVE_DEV_DATA", "false").lower()
return preserve in ["true", "1", "yes"]
def log_api_key_warning() -> None:
"""
Log warning if production API keys are detected in DEV mode
Checks for common API key environment variables and warns if found.
"""
if not is_dev_mode():
return
# List of API key environment variables to check
api_key_vars = [
"OPENAI_API_KEY",
"ANTHROPIC_API_KEY",
"ALPHAADVANTAGE_API_KEY",
"JINA_API_KEY"
]
detected_keys = []
for var in api_key_vars:
value = os.getenv(var)
if value and value != "" and "your_" not in value.lower():
detected_keys.append(var)
if detected_keys:
print("⚠️ WARNING: Production API keys detected in DEV mode")
print(f" Detected: {', '.join(detected_keys)}")
print(" These keys will NOT be used - mock AI responses will be returned")
print(" This is expected if you're testing dev mode with existing .env file")
def get_deployment_mode_dict() -> dict:
"""
Get deployment mode information as dictionary (for API responses)
Returns:
Dictionary with deployment mode metadata
"""
return {
"deployment_mode": get_deployment_mode(),
"is_dev_mode": is_dev_mode(),
"preserve_dev_data": should_preserve_dev_data() if is_dev_mode() else None
}
Step 4: Run tests to verify they pass
Run: pytest tests/unit/test_deployment_config.py -v
Expected: PASS (10 tests)
Step 5: Commit
git add tools/deployment_config.py tests/unit/test_deployment_config.py
git commit -m "feat: add deployment mode configuration utilities"
Task 5: Add Dev Database Initialization and Cleanup
Files:
- Modify:
api/database.py:42-213 - Create:
tests/unit/test_dev_database.py
Step 1: Write tests for dev database handling
Create tests/unit/test_dev_database.py:
import os
import pytest
from pathlib import Path
from api.database import initialize_dev_database, cleanup_dev_database
def test_initialize_dev_database_creates_fresh_db(tmp_path):
"""Test dev database initialization creates clean schema"""
db_path = str(tmp_path / "test_dev.db")
# Create initial database with some data
from api.database import get_db_connection, initialize_database
initialize_database(db_path)
conn = get_db_connection(db_path)
conn.execute("INSERT INTO jobs (job_id, config_path, status, date_range, models, created_at) VALUES (?, ?, ?, ?, ?, ?)",
("test-job", "config.json", "completed", "2025-01-01:2025-01-31", '["model1"]', "2025-01-01T00:00:00"))
conn.commit()
conn.close()
# Verify data exists
conn = get_db_connection(db_path)
cursor = conn.cursor()
cursor.execute("SELECT COUNT(*) FROM jobs")
assert cursor.fetchone()[0] == 1
conn.close()
# Initialize dev database (should reset)
initialize_dev_database(db_path)
# Verify data is cleared
conn = get_db_connection(db_path)
cursor = conn.cursor()
cursor.execute("SELECT COUNT(*) FROM jobs")
assert cursor.fetchone()[0] == 0
conn.close()
def test_cleanup_dev_database_removes_files(tmp_path):
"""Test dev cleanup removes database and data files"""
# Setup dev files
db_path = str(tmp_path / "test_dev.db")
data_path = str(tmp_path / "dev_agent_data")
Path(db_path).touch()
Path(data_path).mkdir(parents=True, exist_ok=True)
(Path(data_path) / "test_file.jsonl").touch()
# Verify files exist
assert Path(db_path).exists()
assert Path(data_path).exists()
# Cleanup
cleanup_dev_database(db_path, data_path)
# Verify files removed
assert not Path(db_path).exists()
assert not Path(data_path).exists()
def test_initialize_dev_respects_preserve_flag(tmp_path):
"""Test that PRESERVE_DEV_DATA flag prevents cleanup"""
os.environ["PRESERVE_DEV_DATA"] = "true"
db_path = str(tmp_path / "test_dev.db")
# Create database with data
from api.database import get_db_connection, initialize_database
initialize_database(db_path)
conn = get_db_connection(db_path)
conn.execute("INSERT INTO jobs (job_id, config_path, status, date_range, models, created_at) VALUES (?, ?, ?, ?, ?, ?)",
("test-job", "config.json", "completed", "2025-01-01:2025-01-31", '["model1"]', "2025-01-01T00:00:00"))
conn.commit()
conn.close()
# Initialize with preserve flag
initialize_dev_database(db_path)
# Verify data is preserved
conn = get_db_connection(db_path)
cursor = conn.cursor()
cursor.execute("SELECT COUNT(*) FROM jobs")
assert cursor.fetchone()[0] == 1
conn.close()
os.environ.pop("PRESERVE_DEV_DATA")
Step 2: Run test to verify it fails
Run: pytest tests/unit/test_dev_database.py -v
Expected: FAIL with "ImportError: cannot import name 'initialize_dev_database'"
Step 3: Add dev database functions to database.py
Add to api/database.py after line 213 (after initialize_database function):
def initialize_dev_database(db_path: str = "data/trading_dev.db") -> None:
"""
Initialize dev database with clean schema
Deletes and recreates dev database unless PRESERVE_DEV_DATA=true.
Used at startup in DEV mode to ensure clean testing environment.
Args:
db_path: Path to dev database file
"""
from tools.deployment_config import should_preserve_dev_data
if should_preserve_dev_data():
print(f"ℹ️ PRESERVE_DEV_DATA=true, keeping existing dev database: {db_path}")
# Ensure schema exists even if preserving data
if not Path(db_path).exists():
print(f"📁 Dev database doesn't exist, creating: {db_path}")
initialize_database(db_path)
return
# Delete existing dev database
if Path(db_path).exists():
print(f"🗑️ Removing existing dev database: {db_path}")
Path(db_path).unlink()
# Create fresh dev database
print(f"📁 Creating fresh dev database: {db_path}")
initialize_database(db_path)
def cleanup_dev_database(db_path: str = "data/trading_dev.db", data_path: str = "./data/dev_agent_data") -> None:
"""
Cleanup dev database and data files
Args:
db_path: Path to dev database file
data_path: Path to dev data directory
"""
import shutil
# Remove dev database
if Path(db_path).exists():
print(f"🗑️ Removing dev database: {db_path}")
Path(db_path).unlink()
# Remove dev data directory
if Path(data_path).exists():
print(f"🗑️ Removing dev data directory: {data_path}")
shutil.rmtree(data_path)
Step 4: Run tests to verify they pass
Run: pytest tests/unit/test_dev_database.py -v
Expected: PASS (3 tests)
Step 5: Commit
git add api/database.py tests/unit/test_dev_database.py
git commit -m "feat: add dev database initialization and cleanup functions"
Task 6: Update Database Module to Support Deployment Modes
Files:
- Modify:
api/database.py:16-39
Step 1: Write test for automatic db path resolution
Add to tests/unit/test_dev_database.py:
def test_get_db_connection_resolves_dev_path():
"""Test that get_db_connection uses dev path in DEV mode"""
import os
os.environ["DEPLOYMENT_MODE"] = "DEV"
# This should automatically resolve to dev database
# We're just testing the path logic, not actually creating DB
from api.database import resolve_db_path
prod_path = "data/trading.db"
dev_path = resolve_db_path(prod_path)
assert dev_path == "data/trading_dev.db"
os.environ["DEPLOYMENT_MODE"] = "PROD"
Step 2: Run test to verify it fails
Run: pytest tests/unit/test_dev_database.py::test_get_db_connection_resolves_dev_path -v
Expected: FAIL with "ImportError: cannot import name 'resolve_db_path'"
Step 3: Update database module with deployment mode support
Modify api/database.py, add import at top after line 13:
from tools.deployment_config import get_db_path
Modify get_db_connection function (lines 16-39):
def get_db_connection(db_path: str = "data/jobs.db") -> sqlite3.Connection:
"""
Get SQLite database connection with proper configuration.
Automatically resolves to dev database if DEPLOYMENT_MODE=DEV.
Args:
db_path: Path to SQLite database file
Returns:
Configured SQLite connection
Configuration:
- Foreign keys enabled for referential integrity
- Row factory for dict-like access
- Check same thread disabled for FastAPI async compatibility
"""
# Resolve path based on deployment mode
resolved_path = get_db_path(db_path)
# Ensure data directory exists
db_path_obj = Path(resolved_path)
db_path_obj.parent.mkdir(parents=True, exist_ok=True)
conn = sqlite3.connect(resolved_path, check_same_thread=False)
conn.execute("PRAGMA foreign_keys = ON")
conn.row_factory = sqlite3.Row
return conn
def resolve_db_path(db_path: str) -> str:
"""
Resolve database path based on deployment mode
Convenience function for testing.
Args:
db_path: Base database path
Returns:
Resolved path (dev or prod)
"""
return get_db_path(db_path)
Step 4: Run test to verify it passes
Run: pytest tests/unit/test_dev_database.py::test_get_db_connection_resolves_dev_path -v
Expected: PASS
Step 5: Run all database tests
Run: pytest tests/unit/test_database.py tests/unit/test_dev_database.py -v
Expected: All tests PASS
Step 6: Commit
git add api/database.py
git commit -m "feat: integrate deployment mode path resolution in database module"
Task 7: Update BaseAgent to Support Mock AI Provider
Files:
- Modify:
agent/base_agent/base_agent.py:146-189
Step 1: Write test for BaseAgent mock integration
Create tests/unit/test_base_agent_mock.py:
import os
import pytest
import asyncio
from agent.base_agent.base_agent import BaseAgent
def test_base_agent_uses_mock_in_dev_mode():
"""Test BaseAgent uses mock model when DEPLOYMENT_MODE=DEV"""
os.environ["DEPLOYMENT_MODE"] = "DEV"
agent = BaseAgent(
signature="test-agent",
basemodel="mock/test-trader",
log_path="./data/dev_agent_data"
)
# Initialize should create mock model
asyncio.run(agent.initialize())
assert agent.model is not None
assert "Mock" in str(type(agent.model))
os.environ["DEPLOYMENT_MODE"] = "PROD"
def test_base_agent_warns_about_api_keys_in_dev(capsys):
"""Test BaseAgent logs warning about API keys in DEV mode"""
os.environ["DEPLOYMENT_MODE"] = "DEV"
os.environ["OPENAI_API_KEY"] = "sk-test123"
agent = BaseAgent(
signature="test-agent",
basemodel="mock/test-trader"
)
asyncio.run(agent.initialize())
captured = capsys.readouterr()
assert "WARNING" in captured.out or "DEV" in captured.out
os.environ.pop("OPENAI_API_KEY")
os.environ["DEPLOYMENT_MODE"] = "PROD"
def test_base_agent_uses_dev_data_path():
"""Test BaseAgent uses dev data paths in DEV mode"""
os.environ["DEPLOYMENT_MODE"] = "DEV"
agent = BaseAgent(
signature="test-agent",
basemodel="mock/test-trader",
log_path="./data/agent_data" # Original path
)
# Should be converted to dev path
assert "dev_agent_data" in agent.base_log_path
os.environ["DEPLOYMENT_MODE"] = "PROD"
Step 2: Run test to verify it fails
Run: pytest tests/unit/test_base_agent_mock.py -v
Expected: FAIL (BaseAgent doesn't use mock yet)
Step 3: Update BaseAgent init to handle deployment mode
Modify agent/base_agent/base_agent.py, add imports after line 25:
from tools.deployment_config import (
is_dev_mode,
get_data_path,
log_api_key_warning,
get_deployment_mode
)
Modify __init__ method around line 103 to update log path:
# Set log path (apply deployment mode path resolution)
self.base_log_path = get_data_path(log_path or "./data/agent_data")
Step 4: Update BaseAgent initialize() to use mock in dev mode
Modify initialize method (lines 146-189):
async def initialize(self) -> None:
"""Initialize MCP client and AI model"""
print(f"🚀 Initializing agent: {self.signature}")
print(f"🔧 Deployment mode: {get_deployment_mode()}")
# Log API key warning if in dev mode
log_api_key_warning()
# Validate OpenAI configuration (only in PROD mode)
if not is_dev_mode():
if not self.openai_api_key:
raise ValueError("❌ OpenAI API key not set. Please configure OPENAI_API_KEY in environment or config file.")
if not self.openai_base_url:
print("⚠️ OpenAI base URL not set, using default")
try:
# Create MCP client
self.client = MultiServerMCPClient(self.mcp_config)
# Get tools
self.tools = await self.client.get_tools()
if not self.tools:
print("⚠️ Warning: No MCP tools loaded. MCP services may not be running.")
print(f" MCP configuration: {self.mcp_config}")
else:
print(f"✅ Loaded {len(self.tools)} MCP tools")
except Exception as e:
raise RuntimeError(
f"❌ Failed to initialize MCP client: {e}\n"
f" Please ensure MCP services are running at the configured ports.\n"
f" Run: python agent_tools/start_mcp_services.py"
)
try:
# Create AI model (mock in DEV mode, real in PROD mode)
if is_dev_mode():
from agent.mock_provider import MockChatModel
self.model = MockChatModel(date="2025-01-01") # Date will be updated per session
print(f"🤖 Using MockChatModel (DEV mode)")
else:
self.model = ChatOpenAI(
model=self.basemodel,
base_url=self.openai_base_url,
api_key=self.openai_api_key,
max_retries=3,
timeout=30
)
print(f"🤖 Using {self.basemodel} (PROD mode)")
except Exception as e:
raise RuntimeError(f"❌ Failed to initialize AI model: {e}")
# Note: agent will be created in run_trading_session() based on specific date
# because system_prompt needs the current date and price information
print(f"✅ Agent {self.signature} initialization completed")
Step 5: Update run_trading_session to set date on mock model
Modify run_trading_session method around line 236:
async def run_trading_session(self, today_date: str) -> None:
"""
Run single day trading session
Args:
today_date: Trading date
"""
print(f"📈 Starting trading session: {today_date}")
# Update mock model date if in dev mode
if is_dev_mode():
self.model.date = today_date
# Set up logging
log_file = self._setup_logging(today_date)
# Update system prompt
self.agent = create_agent(
self.model,
tools=self.tools,
system_prompt=get_agent_system_prompt(today_date, self.signature),
)
# ... rest of method unchanged
Step 6: Run tests to verify they pass
Run: pytest tests/unit/test_base_agent_mock.py -v
Expected: PASS (3 tests)
Step 7: Commit
git add agent/base_agent/base_agent.py tests/unit/test_base_agent_mock.py
git commit -m "feat: integrate mock AI provider in BaseAgent for DEV mode"
Task 8: Update Main Entry Point with Dev Database Initialization
Files:
- Modify:
main.py:94-110
Step 1: Add import statements
Add after line 11 in main.py:
from tools.deployment_config import (
is_dev_mode,
get_deployment_mode,
log_api_key_warning
)
from api.database import initialize_dev_database
Step 2: Add dev initialization before main loop
Modify main function, add after line 101 (after config is loaded):
# Initialize dev environment if needed
if is_dev_mode():
print("=" * 60)
print("🛠️ DEVELOPMENT MODE ACTIVE")
print("=" * 60)
log_api_key_warning()
# Initialize dev database (reset unless PRESERVE_DEV_DATA=true)
from tools.deployment_config import get_db_path
dev_db_path = get_db_path("data/jobs.db")
initialize_dev_database(dev_db_path)
print("=" * 60)
Step 3: Test dev mode initialization manually
Run: DEPLOYMENT_MODE=DEV python main.py configs/default_config.json
Expected: Prints "DEVELOPMENT MODE ACTIVE" and initializes dev database
Step 4: Verify database files
Run: ls -la data/*.db
Expected: Shows jobs_dev.db file
Step 5: Commit
git add main.py
git commit -m "feat: add dev mode initialization to main entry point"
Task 9: Update API to Include Deployment Mode Flag
Files:
- Modify:
api/main.py(find API response locations) - Create:
tests/integration/test_api_deployment_flag.py
Step 1: Find API response generation locations
Run: grep -n "return.*job" api/main.py | head -20
Step 2: Write test for API deployment mode flag
Create tests/integration/test_api_deployment_flag.py:
import os
import pytest
from fastapi.testclient import TestClient
def test_api_includes_deployment_mode_flag():
"""Test API responses include deployment_mode field"""
os.environ["DEPLOYMENT_MODE"] = "DEV"
from api.main import app
client = TestClient(app)
# Test GET /health endpoint (should include deployment info)
response = client.get("/health")
assert response.status_code == 200
data = response.json()
assert "deployment_mode" in data
assert data["deployment_mode"] == "DEV"
def test_job_response_includes_deployment_mode():
"""Test job creation response includes deployment mode"""
os.environ["DEPLOYMENT_MODE"] = "PROD"
from api.main import app
client = TestClient(app)
# Create a test job
config = {
"agent_type": "BaseAgent",
"date_range": {"init_date": "2025-01-01", "end_date": "2025-01-02"},
"models": [{"name": "test", "basemodel": "mock/test", "signature": "test", "enabled": True}]
}
response = client.post("/run", json={"config": config})
if response.status_code == 200:
data = response.json()
assert "deployment_mode" in data
assert data["deployment_mode"] == "PROD"
Step 3: Run test to verify it fails
Run: pytest tests/integration/test_api_deployment_flag.py -v
Expected: FAIL (deployment_mode not in response)
Step 4: Update API responses to include deployment mode
Find api/main.py and locate response return statements. Add deployment mode to responses.
For /health endpoint:
@app.get("/health")
async def health_check():
"""Health check endpoint"""
from tools.deployment_config import get_deployment_mode_dict
return {
"status": "healthy",
**get_deployment_mode_dict()
}
For job-related endpoints, add to response dict:
from tools.deployment_config import get_deployment_mode_dict
# In response returns, add:
{
# ... existing fields
**get_deployment_mode_dict()
}
Step 5: Run test to verify it passes
Run: pytest tests/integration/test_api_deployment_flag.py -v
Expected: PASS
Step 6: Commit
git add api/main.py tests/integration/test_api_deployment_flag.py
git commit -m "feat: add deployment_mode flag to API responses"
Task 10: Update Documentation
Files:
- Modify:
README.md - Modify:
API_REFERENCE.md - Modify:
CLAUDE.md
Step 1: Update README with dev mode section
Add to README.md after the "Configuration" section:
## Development Mode
AI-Trader supports a development mode that mocks AI API calls for testing without costs.
### Quick Start
```bash
# Set environment variables
export DEPLOYMENT_MODE=DEV
export PRESERVE_DEV_DATA=false
# Run simulation (uses mock AI, isolated dev database)
python main.py configs/default_config.json
How It Works
DEPLOYMENT_MODE=DEV:
- Mock AI responses (no API calls to OpenAI/Anthropic)
- Separate database:
data/trading_dev.db - Separate data directory:
data/dev_agent_data/ - Dev database reset on startup (unless PRESERVE_DEV_DATA=true)
- Warnings logged if production API keys detected
DEPLOYMENT_MODE=PROD (default):
- Real AI API calls
- Production database:
data/trading.db - Production data directory:
data/agent_data/
Mock AI Behavior
The mock provider returns deterministic responses that rotate through stocks:
- Day 1: AAPL
- Day 2: MSFT
- Day 3: GOOGL
- Etc. (cycles through 10 stocks)
Each mock response includes:
- Price queries for selected stock
- Buy order for 5 shares
- Finish signal to end session
Environment Variables
DEPLOYMENT_MODE=PROD # PROD or DEV (default: PROD)
PRESERVE_DEV_DATA=false # Keep dev data between runs (default: false)
Use Cases
- Orchestration testing: Verify agent loop, position tracking, logging
- CI/CD pipelines: Run tests without API costs
- Configuration validation: Test date ranges, model configs
- Development iteration: Rapid testing of code changes
Limitations
- Mock responses are static (not context-aware)
- No actual market analysis
- Fixed trading pattern
- For logic testing only, not trading strategy validation
**Step 2: Update API_REFERENCE.md**
Add section after "Response Format":
```markdown
### Deployment Mode
All API responses include a `deployment_mode` field:
```json
{
"job_id": "abc123",
"status": "completed",
"deployment_mode": "DEV",
"is_dev_mode": true,
"preserve_dev_data": false
}
Fields:
deployment_mode: "PROD" or "DEV"is_dev_mode: Boolean flagpreserve_dev_data: Null in PROD, boolean in DEV
DEV Mode Behavior:
- No AI API calls (mock responses)
- Separate dev database (
jobs_dev.db) - Separate data directory (
dev_agent_data/) - Database reset on startup (unless PRESERVE_DEV_DATA=true)
**Step 3: Update CLAUDE.md**
Add section to "Important Implementation Details":
```markdown
### Development Mode
**Deployment Modes:**
- `DEPLOYMENT_MODE=PROD`: Real AI calls, production data paths
- `DEPLOYMENT_MODE=DEV`: Mock AI, isolated dev environment
**DEV Mode Characteristics:**
- Uses `MockChatModel` from `agent/mock_provider/`
- Data paths: `data/dev_agent_data/` and `data/trading_dev.db`
- Dev database reset on startup (controlled by `PRESERVE_DEV_DATA`)
- API responses flagged with `deployment_mode` field
**Implementation Details:**
- Deployment config: `tools/deployment_config.py`
- Mock provider: `agent/mock_provider/mock_ai_provider.py`
- LangChain wrapper: `agent/mock_provider/mock_langchain_model.py`
- BaseAgent integration: `agent/base_agent/base_agent.py:146-189`
- Database handling: `api/database.py` (automatic path resolution)
**Testing Dev Mode:**
```bash
DEPLOYMENT_MODE=DEV python main.py configs/default_config.json
**Step 4: Verify documentation changes**
Run: `grep -n "DEPLOYMENT_MODE" README.md API_REFERENCE.md CLAUDE.md`
Expected: Shows added sections in all three files
**Step 5: Commit**
```bash
git add README.md API_REFERENCE.md CLAUDE.md
git commit -m "docs: add development mode documentation"
Task 11: Integration Testing
Files:
- Create:
tests/integration/test_dev_mode_e2e.py
Step 1: Write end-to-end dev mode test
Create tests/integration/test_dev_mode_e2e.py:
import os
import json
import pytest
import asyncio
from pathlib import Path
from agent.base_agent.base_agent import BaseAgent
@pytest.fixture
def dev_mode_env():
"""Setup and teardown for dev mode testing"""
# Setup
original_mode = os.environ.get("DEPLOYMENT_MODE")
os.environ["DEPLOYMENT_MODE"] = "DEV"
os.environ["PRESERVE_DEV_DATA"] = "false"
yield
# Teardown
if original_mode:
os.environ["DEPLOYMENT_MODE"] = original_mode
else:
os.environ.pop("DEPLOYMENT_MODE", None)
os.environ.pop("PRESERVE_DEV_DATA", None)
def test_dev_mode_full_simulation(dev_mode_env, tmp_path):
"""Test complete simulation run in dev mode"""
# Setup config
config = {
"agent_type": "BaseAgent",
"date_range": {
"init_date": "2025-01-01",
"end_date": "2025-01-03"
},
"models": [{
"name": "test-model",
"basemodel": "mock/test-trader",
"signature": "test-dev-agent",
"enabled": True
}],
"agent_config": {
"max_steps": 5,
"max_retries": 1,
"base_delay": 0.1,
"initial_cash": 10000.0
},
"log_config": {
"log_path": str(tmp_path / "dev_agent_data")
}
}
# Create agent
model_config = config["models"][0]
agent = BaseAgent(
signature=model_config["signature"],
basemodel=model_config["basemodel"],
log_path=config["log_config"]["log_path"],
max_steps=config["agent_config"]["max_steps"],
initial_cash=config["agent_config"]["initial_cash"],
init_date=config["date_range"]["init_date"]
)
# Initialize and run
asyncio.run(agent.initialize())
# Verify mock model
assert agent.model is not None
assert "Mock" in str(type(agent.model))
# Run single day
asyncio.run(agent.run_trading_session("2025-01-01"))
# Verify logs created
log_path = Path(agent.base_log_path) / agent.signature / "log" / "2025-01-01" / "log.jsonl"
assert log_path.exists()
# Verify log content
with open(log_path, "r") as f:
logs = [json.loads(line) for line in f]
assert len(logs) > 0
assert any("AAPL" in str(log) for log in logs) # Day 1 should mention AAPL
def test_dev_database_isolation(dev_mode_env, tmp_path):
"""Test dev and prod databases are separate"""
from api.database import get_db_connection, initialize_database
# Initialize prod database
prod_db = str(tmp_path / "test_prod.db")
initialize_database(prod_db)
conn = get_db_connection(prod_db)
conn.execute("INSERT INTO jobs (job_id, config_path, status, date_range, models, created_at) VALUES (?, ?, ?, ?, ?, ?)",
("prod-job", "config.json", "running", "2025-01-01:2025-01-31", '["model1"]', "2025-01-01T00:00:00"))
conn.commit()
conn.close()
# Initialize dev database (different path)
dev_db = str(tmp_path / "test_dev.db")
from api.database import initialize_dev_database
initialize_dev_database(dev_db)
# Verify prod data still exists
conn = get_db_connection(prod_db)
cursor = conn.cursor()
cursor.execute("SELECT COUNT(*) FROM jobs WHERE job_id = 'prod-job'")
assert cursor.fetchone()[0] == 1
conn.close()
# Verify dev database is empty
conn = get_db_connection(dev_db)
cursor = conn.cursor()
cursor.execute("SELECT COUNT(*) FROM jobs")
assert cursor.fetchone()[0] == 0
conn.close()
def test_preserve_dev_data_flag(dev_mode_env, tmp_path):
"""Test PRESERVE_DEV_DATA prevents cleanup"""
os.environ["PRESERVE_DEV_DATA"] = "true"
from api.database import initialize_dev_database, get_db_connection
dev_db = str(tmp_path / "test_dev_preserve.db")
# Create database with data
from api.database import initialize_database
initialize_database(dev_db)
conn = get_db_connection(dev_db)
conn.execute("INSERT INTO jobs (job_id, config_path, status, date_range, models, created_at) VALUES (?, ?, ?, ?, ?, ?)",
("dev-job-1", "config.json", "completed", "2025-01-01:2025-01-31", '["model1"]', "2025-01-01T00:00:00"))
conn.commit()
conn.close()
# Initialize again (should preserve)
initialize_dev_database(dev_db)
# Verify data preserved
conn = get_db_connection(dev_db)
cursor = conn.cursor()
cursor.execute("SELECT COUNT(*) FROM jobs WHERE job_id = 'dev-job-1'")
assert cursor.fetchone()[0] == 1
conn.close()
Step 2: Run integration tests
Run: pytest tests/integration/test_dev_mode_e2e.py -v -s
Expected: PASS (3 tests)
Step 3: Run full test suite
Run: pytest tests/ -v
Expected: All tests PASS
Step 4: Commit
git add tests/integration/test_dev_mode_e2e.py
git commit -m "test: add end-to-end integration tests for dev mode"
Task 12: Manual Verification and Final Testing
Files:
- N/A (manual testing)
Step 1: Test dev mode startup
Run:
export DEPLOYMENT_MODE=DEV
python main.py configs/default_config.json
Verify output shows:
- "DEVELOPMENT MODE ACTIVE"
- "Using MockChatModel (DEV mode)"
- Warning about API keys (if .env has keys)
- Creates
data/trading_dev.db - Creates
data/dev_agent_data/
Step 2: Test prod mode (default)
Run:
unset DEPLOYMENT_MODE
python main.py configs/default_config.json
Verify output shows:
- No "DEVELOPMENT MODE" message
- "Using [actual model] (PROD mode)"
- Uses
data/trading.db - Uses
data/agent_data/
Step 3: Test preserve flag
Run:
export DEPLOYMENT_MODE=DEV
export PRESERVE_DEV_DATA=true
python main.py configs/default_config.json
# Run again
python main.py configs/default_config.json
Verify:
- Second run shows "PRESERVE_DEV_DATA=true"
- Dev database not deleted between runs
- Position data persists
Step 4: Verify database isolation
Run:
ls -la data/*.db
sqlite3 data/trading_dev.db "SELECT COUNT(*) FROM jobs"
sqlite3 data/trading.db "SELECT COUNT(*) FROM jobs"
Verify:
- Both databases exist
- Contain different data
- Dev database can be deleted without affecting prod
Step 5: Test API with deployment flag
Run:
export DEPLOYMENT_MODE=DEV
uvicorn api.main:app --reload
# In another terminal:
curl http://localhost:8000/health
Verify response includes:
{
"status": "healthy",
"deployment_mode": "DEV",
"is_dev_mode": true,
"preserve_dev_data": false
}
Step 6: Document any issues found
Create issue tickets for any bugs discovered during manual testing.
Step 7: Final commit
# If any fixes were needed during manual testing:
git add .
git commit -m "fix: address issues found during manual verification"
# Tag the feature
git tag -a v0.1.0-dev-mode -m "Add development mode with mock AI provider"
Summary
This implementation adds a complete development mode feature to AI-Trader:
✅ Environment Configuration
DEPLOYMENT_MODE(PROD/DEV)PRESERVE_DEV_DATAflag- Documentation in
.env.example
✅ Mock AI Provider
- Deterministic stock rotation
- LangChain-compatible wrapper
- No API costs in DEV mode
✅ Data Isolation
- Separate dev database (
trading_dev.db) - Separate dev data directory (
dev_agent_data/) - Automatic path resolution
✅ Database Management
- Dev database reset on startup
- Preserve flag for debugging
- Cleanup utilities
✅ Integration
- BaseAgent mock integration
- Main entry point initialization
- API deployment mode flag
✅ Testing
- Unit tests for all components
- Integration tests for E2E flows
- Manual verification checklist
✅ Documentation
- README with dev mode guide
- API reference updates
- CLAUDE.md implementation notes
Total Tasks: 12 Estimated Time: 2-3 hours (bite-sized tasks, frequent commits) Test Coverage: Unit + Integration + Manual
Key Design Decisions:
- Deployment mode controlled by environment variable (not config file)
- Automatic path resolution (transparent to existing code)
- Mock provider uses rotation for test variety
- Preserve flag for debugging (default: false for clean slate)
- API responses flagged for observability