mirror of
https://github.com/farcasclaudiu/TradingAgents.git
synced 2026-06-22 07:01:21 +03:00
chore: release v0.2.4 — structured agents, checkpoint, memory log, providers
This release bundles substantial work since v0.2.3: - Structured-output Research Manager, Trader, and Portfolio Manager (canonical with_structured_output pattern, single LLM call per agent, rendered markdown preserves the existing report shape). - LangGraph checkpoint resume for crash recovery (--checkpoint flag). - Persistent decision log replacing the per-agent BM25 memory, with deferred reflection driven by yfinance returns + alpha vs SPY. - DeepSeek, Qwen, GLM, and Azure OpenAI provider support; dynamic OpenRouter model selection. - Docker support; cache and logs moved to ~/.tradingagents/ to fix Docker permission issues. - Windows UTF-8 encoding fix on every file I/O site. - 5-tier rating consistency (Buy / Overweight / Hold / Underweight / Sell) across Research Manager, Portfolio Manager, signal processor, memory log. Plus the small quality items in this commit: 1. Suppress noisy Pydantic serializer warnings from OpenAI Responses-API parse path by defaulting structured-output to method="function_calling" (root-cause fix, not a warnings filter — same typed result, no warnings). 2. Ship scripts/smoke_structured_output.py so contributors can verify their provider's structured-output path with one command. 3. Add opt-in memory_log_max_entries config — when set, oldest resolved memory log entries are pruned once the cap is exceeded; pending entries (unresolved) are never pruned. 4. backend_url default changed from the OpenAI URL to None so the per-provider client falls back to its native endpoint instead of leaking OpenAI's URL into Gemini / other clients. CHANGELOG.md added with the full v0.2.4 entry. 92 tests pass without API keys.
This commit is contained in:
@@ -17,11 +17,14 @@ class TradingMemoryLog:
|
||||
_REFLECTION_RE = re.compile(r"REFLECTION:\n(.*?)$", re.DOTALL)
|
||||
|
||||
def __init__(self, config: dict = None):
|
||||
cfg = config or {}
|
||||
self._log_path = None
|
||||
path = (config or {}).get("memory_log_path")
|
||||
path = cfg.get("memory_log_path")
|
||||
if path:
|
||||
self._log_path = Path(path).expanduser()
|
||||
self._log_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
# Optional cap on resolved entries. None disables rotation.
|
||||
self._max_entries = cfg.get("memory_log_max_entries")
|
||||
|
||||
# --- Write path (Phase A) ---
|
||||
|
||||
@@ -153,6 +156,7 @@ class TradingMemoryLog:
|
||||
if not updated:
|
||||
return
|
||||
|
||||
new_blocks = self._apply_rotation(new_blocks)
|
||||
new_text = self._SEPARATOR.join(new_blocks)
|
||||
tmp_path = self._log_path.with_suffix(".tmp")
|
||||
tmp_path.write_text(new_text, encoding="utf-8")
|
||||
@@ -206,6 +210,7 @@ class TradingMemoryLog:
|
||||
if not matched:
|
||||
new_blocks.append(block)
|
||||
|
||||
new_blocks = self._apply_rotation(new_blocks)
|
||||
new_text = self._SEPARATOR.join(new_blocks)
|
||||
tmp_path = self._log_path.with_suffix(".tmp")
|
||||
tmp_path.write_text(new_text, encoding="utf-8")
|
||||
@@ -213,6 +218,43 @@ class TradingMemoryLog:
|
||||
|
||||
# --- Helpers ---
|
||||
|
||||
def _apply_rotation(self, blocks: List[str]) -> List[str]:
|
||||
"""Drop oldest resolved blocks when their count exceeds max_entries.
|
||||
|
||||
Pending blocks are always kept (they represent unprocessed work).
|
||||
Returns ``blocks`` unchanged when rotation is disabled or under cap.
|
||||
"""
|
||||
if not self._max_entries or self._max_entries <= 0:
|
||||
return blocks
|
||||
|
||||
# Tag each block with (kept, is_resolved) by parsing tag-line markers.
|
||||
decisions = []
|
||||
for block in blocks:
|
||||
stripped = block.strip()
|
||||
if not stripped:
|
||||
decisions.append((block, False))
|
||||
continue
|
||||
tag_line = stripped.splitlines()[0].strip()
|
||||
is_resolved = (
|
||||
tag_line.startswith("[")
|
||||
and tag_line.endswith("]")
|
||||
and not tag_line.endswith("| pending]")
|
||||
)
|
||||
decisions.append((block, is_resolved))
|
||||
|
||||
resolved_count = sum(1 for _, r in decisions if r)
|
||||
if resolved_count <= self._max_entries:
|
||||
return blocks
|
||||
|
||||
to_drop = resolved_count - self._max_entries
|
||||
kept: List[str] = []
|
||||
for block, is_resolved in decisions:
|
||||
if is_resolved and to_drop > 0:
|
||||
to_drop -= 1
|
||||
continue
|
||||
kept.append(block)
|
||||
return kept
|
||||
|
||||
def _parse_entry(self, raw: str) -> Optional[dict]:
|
||||
lines = raw.strip().splitlines()
|
||||
if not lines:
|
||||
|
||||
@@ -7,6 +7,10 @@ DEFAULT_CONFIG = {
|
||||
"results_dir": os.getenv("TRADINGAGENTS_RESULTS_DIR", os.path.join(_TRADINGAGENTS_HOME, "logs")),
|
||||
"data_cache_dir": os.getenv("TRADINGAGENTS_CACHE_DIR", os.path.join(_TRADINGAGENTS_HOME, "cache")),
|
||||
"memory_log_path": os.getenv("TRADINGAGENTS_MEMORY_LOG_PATH", os.path.join(_TRADINGAGENTS_HOME, "memory", "trading_memory.md")),
|
||||
# Optional cap on the number of resolved memory log entries. When set,
|
||||
# the oldest resolved entries are pruned once this limit is exceeded.
|
||||
# Pending entries are never pruned. None disables rotation entirely.
|
||||
"memory_log_max_entries": None,
|
||||
# LLM settings
|
||||
"llm_provider": "openai",
|
||||
"deep_think_llm": "gpt-5.4",
|
||||
|
||||
@@ -18,6 +18,22 @@ class NormalizedChatOpenAI(ChatOpenAI):
|
||||
def invoke(self, input, config=None, **kwargs):
|
||||
return normalize_content(super().invoke(input, config, **kwargs))
|
||||
|
||||
def with_structured_output(self, schema, *, method=None, **kwargs):
|
||||
"""Wrap with structured output, defaulting to function_calling for OpenAI.
|
||||
|
||||
langchain-openai's Responses-API-parse path (the default for json_schema
|
||||
when use_responses_api=True) calls response.model_dump(...) on the OpenAI
|
||||
SDK's union-typed parsed response, which makes Pydantic emit ~20
|
||||
PydanticSerializationUnexpectedValue warnings per call. The function-calling
|
||||
path returns a plain tool-call shape that does not trigger that
|
||||
serialization, so it is the cleaner choice for our combination of
|
||||
use_responses_api=True + with_structured_output. Both paths use OpenAI's
|
||||
strict mode and produce the same typed Pydantic instance.
|
||||
"""
|
||||
if method is None:
|
||||
method = "function_calling"
|
||||
return super().with_structured_output(schema, method=method, **kwargs)
|
||||
|
||||
# Kwargs forwarded from user config to ChatOpenAI
|
||||
_PASSTHROUGH_KWARGS = (
|
||||
"timeout", "max_retries", "reasoning_effort",
|
||||
|
||||
Reference in New Issue
Block a user