From 356a563813df4b5867a778c02d8a9a8b6eac483c Mon Sep 17 00:00:00 2001 From: kevin-bruton Date: Sun, 28 Sep 2025 01:08:12 +0200 Subject: [PATCH] feat: add permanent memory --- README.md | 19 ++++ cli/main.py | 52 +++++---- cli/utils.py | 2 +- memory_store/chroma.sqlite3 | Bin 0 -> 163840 bytes tradingagents/agents/managers/risk_manager.py | 5 +- tradingagents/agents/trader/trader.py | 3 +- tradingagents/agents/utils/memory.py | 105 ++++++++++++------ tradingagents/graph/signal_processing.py | 7 +- 8 files changed, 128 insertions(+), 65 deletions(-) create mode 100644 memory_store/chroma.sqlite3 diff --git a/README.md b/README.md index 03a05480..410229f4 100644 --- a/README.md +++ b/README.md @@ -197,6 +197,25 @@ print(decision) You can view the full list of configurations in `tradingagents/default_config.py`. +## Persistent Memory and Learning + +To allow the agents to learn from the success or failure of previous decisions, TradingAgents includes a persistent memory mechanism. + +Each agent's reflections and the "lessons learned" from past trading sessions are stored on disk. This allows the system to build a rich, searchable history of its actions and their consequences, enabling more informed decisions in the future. + +- **Storage**: The memory is managed by the `FinancialSituationMemory` class in `tradingagents/agents/utils/memory.py` and is persisted to the `./memory_store/` directory using a local ChromaDB database. +- **Learning Loop**: After a trade, a `Reflector` agent analyzes the outcome (profit or loss) and generates a "lesson." This lesson is stored in the memory, linked to the market conditions at the time. Before the next trade, agents query this memory for similar past situations to retrieve relevant lessons, which are then used to inform their decision-making process. + +### Inspecting the Memory + +You can inspect the contents of the persistent memory to see what the agents have learned. To do this, run the memory utility script from the root of the project: + +```bash +python -m tradingagents.agents.utils.memory +``` + +The first time you run this, it will populate the memory with example data. Subsequent runs will load and display the data from the `memory_store` directory, demonstrating that the memory persists across sessions. + ## Contributing We welcome contributions from the community! Whether it's fixing a bug, improving documentation, or suggesting a new feature, your input helps make this project better. If you are interested in this line of research, please consider joining our open-source financial AI research community [Tauric Research](https://tauric.ai/). diff --git a/cli/main.py b/cli/main.py index c148c522..03c1858b 100644 --- a/cli/main.py +++ b/cli/main.py @@ -73,6 +73,10 @@ class MessageBuffer: "final_trade_decision": None, } + def _format_report_content(self, content): + """Ensures content is a string.""" + return str(content) + def add_message(self, message_type, content): timestamp = datetime.datetime.now().strftime("%H:%M:%S") self.messages.append((timestamp, message_type, content)) @@ -100,7 +104,7 @@ class MessageBuffer: for section, content in self.report_sections.items(): if content is not None: latest_section = section - latest_content = content + latest_content = self._format_report_content(content) if latest_section and latest_content: # Format the current section for display @@ -136,35 +140,35 @@ class MessageBuffer: report_parts.append("## Analyst Team Reports") if self.report_sections["market_report"]: report_parts.append( - f"### Market Analysis\n{self.report_sections['market_report']}" + f"### Market Analysis\n{self._format_report_content(self.report_sections['market_report'])}" ) if self.report_sections["sentiment_report"]: report_parts.append( - f"### Social Sentiment\n{self.report_sections['sentiment_report']}" + f"### Social Sentiment\n{self._format_report_content(self.report_sections['sentiment_report'])}" ) if self.report_sections["news_report"]: report_parts.append( - f"### News Analysis\n{self.report_sections['news_report']}" + f"### News Analysis\n{self._format_report_content(self.report_sections['news_report'])}" ) if self.report_sections["fundamentals_report"]: report_parts.append( - f"### Fundamentals Analysis\n{self.report_sections['fundamentals_report']}" + f"### Fundamentals Analysis\n{self._format_report_content(self.report_sections['fundamentals_report'])}" ) # Research Team Reports if self.report_sections["investment_plan"]: report_parts.append("## Research Team Decision") - report_parts.append(f"{self.report_sections['investment_plan']}") + report_parts.append(f"{self._format_report_content(self.report_sections['investment_plan'])}") # Trading Team Reports if self.report_sections["trader_investment_plan"]: report_parts.append("## Trading Team Plan") - report_parts.append(f"{self.report_sections['trader_investment_plan']}") + report_parts.append(f"{self._format_report_content(self.report_sections['trader_investment_plan'])}") # Portfolio Management Decision if self.report_sections["final_trade_decision"]: report_parts.append("## Portfolio Management Decision") - report_parts.append(f"{self.report_sections['final_trade_decision']}") + report_parts.append(f"{self._format_report_content(self.report_sections['final_trade_decision'])}") self.final_report = "\n\n".join(report_parts) if report_parts else None @@ -550,6 +554,10 @@ def display_complete_report(final_state): """Display the complete analysis report with team-based panels.""" console.print("\n[bold green]Complete Analysis Report[/bold green]\n") + def _format_content_for_markdown(content): + """Ensures content is a string.""" + return str(content) + # User Position user_position = final_state.get("user_position", "none") cost_per_trade = final_state.get("cost_per_trade", 0.0) @@ -562,7 +570,7 @@ def display_complete_report(final_state): if final_state.get("market_report"): analyst_reports.append( Panel( - Markdown(final_state["market_report"]), + Markdown(_format_content_for_markdown(final_state["market_report"])), title="Market Analyst", border_style="blue", padding=(1, 2), @@ -573,7 +581,7 @@ def display_complete_report(final_state): if final_state.get("sentiment_report"): analyst_reports.append( Panel( - Markdown(final_state["sentiment_report"]), + Markdown(_format_content_for_markdown(final_state["sentiment_report"])), title="Social Analyst", border_style="blue", padding=(1, 2), @@ -584,7 +592,7 @@ def display_complete_report(final_state): if final_state.get("news_report"): analyst_reports.append( Panel( - Markdown(final_state["news_report"]), + Markdown(_format_content_for_markdown(final_state["news_report"])), title="News Analyst", border_style="blue", padding=(1, 2), @@ -595,7 +603,7 @@ def display_complete_report(final_state): if final_state.get("fundamentals_report"): analyst_reports.append( Panel( - Markdown(final_state["fundamentals_report"]), + Markdown(_format_content_for_markdown(final_state["fundamentals_report"])), title="Fundamentals Analyst", border_style="blue", padding=(1, 2), @@ -621,7 +629,7 @@ def display_complete_report(final_state): if debate_state.get("bull_history"): research_reports.append( Panel( - Markdown(debate_state["bull_history"]), + Markdown(_format_content_for_markdown(debate_state["bull_history"])), title="Bull Researcher", border_style="blue", padding=(1, 2), @@ -632,7 +640,7 @@ def display_complete_report(final_state): if debate_state.get("bear_history"): research_reports.append( Panel( - Markdown(debate_state["bear_history"]), + Markdown(_format_content_for_markdown(debate_state["bear_history"])), title="Bear Researcher", border_style="blue", padding=(1, 2), @@ -643,7 +651,7 @@ def display_complete_report(final_state): if debate_state.get("judge_decision"): research_reports.append( Panel( - Markdown(debate_state["judge_decision"]), + Markdown(_format_content_for_markdown(debate_state["judge_decision"])), title="Research Manager", border_style="blue", padding=(1, 2), @@ -665,7 +673,7 @@ def display_complete_report(final_state): console.print( Panel( Panel( - Markdown(final_state["trader_investment_plan"]), + Markdown(_format_content_for_markdown(final_state["trader_investment_plan"])), title="Trader", border_style="blue", padding=(1, 2), @@ -685,7 +693,7 @@ def display_complete_report(final_state): if risk_state.get("risky_history"): risk_reports.append( Panel( - Markdown(risk_state["risky_history"]), + Markdown(_format_content_for_markdown(risk_state["risky_history"])), title="Aggressive Analyst", border_style="blue", padding=(1, 2), @@ -696,7 +704,7 @@ def display_complete_report(final_state): if risk_state.get("safe_history"): risk_reports.append( Panel( - Markdown(risk_state["safe_history"]), + Markdown(_format_content_for_markdown(risk_state["safe_history"])), title="Conservative Analyst", border_style="blue", padding=(1, 2), @@ -707,7 +715,7 @@ def display_complete_report(final_state): if risk_state.get("neutral_history"): risk_reports.append( Panel( - Markdown(risk_state["neutral_history"]), + Markdown(_format_content_for_markdown(risk_state["neutral_history"])), title="Neutral Analyst", border_style="blue", padding=(1, 2), @@ -729,7 +737,7 @@ def display_complete_report(final_state): console.print( Panel( Panel( - Markdown(risk_state["judge_decision"]), + Markdown(_format_content_for_markdown(risk_state["judge_decision"])), title="Portfolio Manager", border_style="blue", padding=(1, 2), @@ -826,7 +834,7 @@ def run_analysis(): if content: file_name = f"{section_name}.md" with open(report_dir / file_name, "w", encoding="utf-8") as f: - f.write(content) + f.write(str(content)) return wrapper message_buffer.add_message = save_message_decorator(message_buffer, "add_message") @@ -836,7 +844,7 @@ def run_analysis(): # Now start the display layout layout = create_layout() - with Live(layout, refresh_per_second=4) as live: + with Live(layout, refresh_per_second=1) as live: # Initial display update_display(layout) diff --git a/cli/utils.py b/cli/utils.py index a90b2fc2..bfa9beff 100644 --- a/cli/utils.py +++ b/cli/utils.py @@ -250,7 +250,7 @@ def select_llm_provider() -> tuple[str, str]: ("OpenAI", "https://api.openai.com/v1"), ("Anthropic", "https://api.anthropic.com/"), ("Google", "https://generativelanguage.googleapis.com/v1"), - ("Openrouter", "https://openrouter.ai/api/v1"), + ("OpenRouter", "https://openrouter.ai/api/v1"), ("Ollama", "http://localhost:11434/v1"), ] diff --git a/memory_store/chroma.sqlite3 b/memory_store/chroma.sqlite3 new file mode 100644 index 0000000000000000000000000000000000000000..61f5629e1eb25c01a924447d44d5af6d97f8c3bc GIT binary patch literal 163840 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 str: + def process_signal(self, full_signal: dict) -> str: """ Process a full trading signal to extract the core decision. @@ -25,7 +26,7 @@ class SignalProcessor: "system", "You are an efficient assistant designed to analyze paragraphs or financial reports provided by a group of analysts. Your task is to extract the investment decision: SELL, BUY, or HOLD. Provide only the extracted decision (SELL, BUY, or HOLD) as your output, without adding any additional text or information.", ), - ("human", full_signal), + ("human", json.dumps(full_signal)), ] - return self.quick_thinking_llm.invoke(messages).content + return self.quick_thinking_llm.invoke(messages).content \ No newline at end of file