diff --git a/agent/base_agent/base_agent.py b/agent/base_agent/base_agent.py index 1b16d64..3bb319c 100644 --- a/agent/base_agent/base_agent.py +++ b/agent/base_agent/base_agent.py @@ -246,6 +246,71 @@ class BaseAgent: """Clear conversation history (called at start of each trading day).""" self.conversation_history = [] + async def generate_summary(self, content: str, max_length: int = 200) -> str: + """ + Generate a concise summary of reasoning content. + + Uses the same AI model to summarize its own reasoning. + + Args: + content: Full reasoning content to summarize + max_length: Approximate character limit for summary + + Returns: + 1-2 sentence summary of key decisions and reasoning + """ + # Truncate content to avoid token limits (keep first 2000 chars) + truncated = content[:2000] if len(content) > 2000 else content + + prompt = f"""Summarize the following trading decision in 1-2 sentences (max {max_length} characters), focusing on the key reasoning and actions taken: + +{truncated} + +Summary:""" + + try: + # Use ainvoke for async call + response = await self.model.ainvoke(prompt) + + # Extract content from response + if hasattr(response, 'content'): + summary = response.content.strip() + elif isinstance(response, dict) and 'content' in response: + summary = response['content'].strip() + else: + summary = str(response).strip() + + # Truncate if too long + if len(summary) > max_length: + summary = summary[:max_length-3] + "..." + + return summary + + except Exception as e: + # If summary generation fails, return truncated original + return truncated[:max_length-3] + "..." + + def generate_summary_sync(self, content: str, max_length: int = 200) -> str: + """ + Synchronous wrapper for generate_summary. + + Args: + content: Full reasoning content to summarize + max_length: Approximate character limit for summary + + Returns: + Summary string + """ + import asyncio + + try: + loop = asyncio.get_event_loop() + except RuntimeError: + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + + return loop.run_until_complete(self.generate_summary(content, max_length)) + def _setup_logging(self, today_date: str) -> str: """Set up log file path""" log_path = os.path.join(self.base_log_path, self.signature, 'log', today_date) diff --git a/tests/unit/test_base_agent_summary.py b/tests/unit/test_base_agent_summary.py new file mode 100644 index 0000000..573a525 --- /dev/null +++ b/tests/unit/test_base_agent_summary.py @@ -0,0 +1,84 @@ +"""Tests for BaseAgent summary generation.""" + +import pytest +from agent.base_agent.base_agent import BaseAgent +from agent.mock_provider.mock_langchain_model import MockChatModel + + +@pytest.mark.asyncio +async def test_generate_summary_basic(): + """Should generate summary from content.""" + agent = BaseAgent( + signature="test-agent", + basemodel="test-model" + ) + + # Use mock model for testing + agent.model = MockChatModel(model="test", signature="test") + + content = """Key intermediate steps + +- Read yesterday's positions: all zeros, $10,000 cash +- Analyzed NVDA strong Q2 results, bought 10 shares +- Analyzed AMD AI momentum, bought 6 shares +- Portfolio now 51% cash reserve for volatility management + +""" + + summary = await agent.generate_summary(content) + + assert isinstance(summary, str) + assert len(summary) > 0 + assert len(summary) <= 203 # 200 + "..." + + +def test_generate_summary_sync(): + """Synchronous summary generation should work.""" + agent = BaseAgent( + signature="test-agent", + basemodel="test-model" + ) + agent.model = MockChatModel(model="test", signature="test") + + content = "Bought AAPL 10 shares based on strong earnings." + summary = agent.generate_summary_sync(content) + + assert isinstance(summary, str) + assert len(summary) > 0 + + +@pytest.mark.asyncio +async def test_generate_summary_truncates_long_content(): + """Should truncate very long content before summarizing.""" + agent = BaseAgent( + signature="test-agent", + basemodel="test-model" + ) + agent.model = MockChatModel(model="test", signature="test") + + # Create content > 2000 chars + content = "Analysis: " + ("x" * 3000) + + summary = await agent.generate_summary(content) + + # Summary should be generated (not throw error) + assert isinstance(summary, str) + assert len(summary) <= 203 + + +@pytest.mark.asyncio +async def test_generate_summary_handles_errors(): + """Should handle errors gracefully.""" + agent = BaseAgent( + signature="test-agent", + basemodel="test-model" + ) + + # No model set - will fail + agent.model = None + + content = "Test content" + summary = await agent.generate_summary(content) + + # Should return truncated original on error (with ... appended) + assert summary == "Test content..."