subagent thinking accordion; indicator fixes; script details & edit
This commit is contained in:
@@ -144,10 +144,25 @@ async def evaluate_indicator(
|
||||
)
|
||||
}))]
|
||||
|
||||
# Get input_series from the indicator's metadata
|
||||
indicator_name = pandas_ta_name[len("custom_"):]
|
||||
# Get input_series from the indicator's metadata.
|
||||
# Look up by the stored pandas_ta_name field (written at creation time),
|
||||
# which is the reliable reverse mapping from ta_name → directory.
|
||||
# Fall back to display-name matching for indicators created before this
|
||||
# field was added.
|
||||
mgr = get_category_manager()
|
||||
read_result = mgr.read("indicator", indicator_name)
|
||||
all_items = mgr.list_items("indicator")
|
||||
read_result = {"exists": False}
|
||||
for item in all_items.get("items", []):
|
||||
meta = item.get("metadata", {})
|
||||
# Primary: match stored pandas_ta_name field (exact, set at write time)
|
||||
if meta.get("pandas_ta_name") == name_lower:
|
||||
read_result = mgr.read("indicator", item.get("name", ""))
|
||||
break
|
||||
# Fallback: infer from display name (legacy indicators without the field)
|
||||
item_name = item.get("name", "")
|
||||
if "custom_" + item_name.lower().replace("-", "_").replace(" ", "_") == name_lower:
|
||||
read_result = mgr.read("indicator", item_name)
|
||||
break
|
||||
if read_result.get("exists") and read_result.get("metadata"):
|
||||
raw_series = read_result["metadata"].get("input_series") or ["close"]
|
||||
input_cols = tuple(raw_series)
|
||||
|
||||
@@ -62,6 +62,12 @@ class BaseMetadata:
|
||||
"""Base metadata for all categories."""
|
||||
name: str # Display name (can have special chars)
|
||||
description: str # LLM-generated description
|
||||
details: str = "" # Full markdown description with enough detail to reproduce the code
|
||||
conda_packages: list[str] = None # Additional conda packages required
|
||||
|
||||
def __post_init__(self):
|
||||
if self.conda_packages is None:
|
||||
self.conda_packages = []
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -69,21 +75,21 @@ class StrategyMetadata(BaseMetadata):
|
||||
"""Metadata for trading strategies."""
|
||||
data_feeds: list[dict] = None # Required data feeds: [{"symbol": "BTC/USDT.BINANCE", "period_seconds": 3600, "description": "..."}]
|
||||
parameters: dict = None # Strategy parameters: {"param_name": {"default": value, "description": "..."}}
|
||||
conda_packages: list[str] = None # Additional conda packages required
|
||||
|
||||
def __post_init__(self):
|
||||
super().__post_init__()
|
||||
if self.data_feeds is None:
|
||||
self.data_feeds = []
|
||||
if self.parameters is None:
|
||||
self.parameters = {}
|
||||
if self.conda_packages is None:
|
||||
self.conda_packages = []
|
||||
|
||||
|
||||
@dataclass
|
||||
class IndicatorMetadata(BaseMetadata):
|
||||
"""Metadata for technical indicators."""
|
||||
conda_packages: list[str] = None # Additional conda packages required
|
||||
# Canonical pandas-ta name, e.g. "custom_trendflex". Set automatically
|
||||
# by CategoryFileManager.write() — do not pass manually.
|
||||
pandas_ta_name: str = ""
|
||||
|
||||
# Fields for TradingView custom study auto-construction:
|
||||
parameters: dict = None
|
||||
@@ -143,8 +149,7 @@ class IndicatorMetadata(BaseMetadata):
|
||||
# Example (RSI levels): [{"id": "ob", "value": 70}, {"id": "os", "value": 30}]
|
||||
|
||||
def __post_init__(self):
|
||||
if self.conda_packages is None:
|
||||
self.conda_packages = []
|
||||
super().__post_init__()
|
||||
if self.input_series is None:
|
||||
self.input_series = ["close"]
|
||||
if self.output_columns is None:
|
||||
@@ -160,11 +165,7 @@ class IndicatorMetadata(BaseMetadata):
|
||||
@dataclass
|
||||
class ResearchMetadata(BaseMetadata):
|
||||
"""Metadata for research scripts."""
|
||||
conda_packages: list[str] = None # Additional conda packages required
|
||||
|
||||
def __post_init__(self):
|
||||
if self.conda_packages is None:
|
||||
self.conda_packages = []
|
||||
pass
|
||||
|
||||
|
||||
# Metadata class registry
|
||||
@@ -503,6 +504,7 @@ class CategoryFileManager:
|
||||
category: str,
|
||||
name: str,
|
||||
description: str,
|
||||
details: str,
|
||||
code: str,
|
||||
metadata: Optional[dict] = None
|
||||
) -> dict[str, Any]:
|
||||
@@ -513,6 +515,7 @@ class CategoryFileManager:
|
||||
category: Category name (strategy, indicator, research)
|
||||
name: Display name for the item
|
||||
description: LLM-generated description (required)
|
||||
details: Full markdown description with enough detail to reproduce the code (required)
|
||||
code: Python implementation code
|
||||
metadata: Additional category-specific metadata fields
|
||||
|
||||
@@ -547,6 +550,13 @@ class CategoryFileManager:
|
||||
meta_dict = metadata or {}
|
||||
meta_dict["name"] = name
|
||||
meta_dict["description"] = description
|
||||
meta_dict["details"] = details
|
||||
|
||||
# For indicators, store the canonical pandas_ta_name so the reverse
|
||||
# mapping (ta_name → directory) is reliable regardless of name casing.
|
||||
if cat == Category.INDICATOR:
|
||||
sanitized = sanitize_name(name).lower()
|
||||
meta_dict["pandas_ta_name"] = f"custom_{sanitized}"
|
||||
|
||||
# Validate and write metadata
|
||||
try:
|
||||
@@ -592,6 +602,8 @@ class CategoryFileManager:
|
||||
code: Optional[str] = None,
|
||||
patches: Optional[list[dict]] = None,
|
||||
description: Optional[str] = None,
|
||||
details: Optional[str] = None,
|
||||
detail_patches: Optional[list[dict]] = None,
|
||||
metadata: Optional[dict] = None
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
@@ -603,6 +615,8 @@ class CategoryFileManager:
|
||||
code: Full Python implementation code to replace existing (optional)
|
||||
patches: List of {old_string, new_string} replacements (optional, preferred for small changes)
|
||||
description: Updated description (optional, omit to keep existing)
|
||||
details: Full replacement for the details field (optional, mutually exclusive with detail_patches)
|
||||
detail_patches: List of {old_string, new_string} replacements applied to the details field (optional)
|
||||
metadata: Additional metadata updates (optional)
|
||||
|
||||
Returns:
|
||||
@@ -614,6 +628,8 @@ class CategoryFileManager:
|
||||
"""
|
||||
if code is not None and patches is not None:
|
||||
return {"success": False, "error": "Provide either 'code' or 'patches', not both"}
|
||||
if details is not None and detail_patches is not None:
|
||||
return {"success": False, "error": "Provide either 'details' or 'detail_patches', not both"}
|
||||
|
||||
try:
|
||||
cat = Category(category)
|
||||
@@ -664,10 +680,25 @@ class CategoryFileManager:
|
||||
except Exception as e:
|
||||
return {"success": False, "error": f"Failed to write implementation: {e}"}
|
||||
|
||||
# Apply text-replacement patches to details field if provided
|
||||
if detail_patches is not None:
|
||||
current_details = existing_meta.get("details", "")
|
||||
for i, patch in enumerate(detail_patches):
|
||||
old = patch.get("old_string", "")
|
||||
new = patch.get("new_string", "")
|
||||
if old not in current_details:
|
||||
return {"success": False, "error": f"Detail patch {i}: old_string not found in details"}
|
||||
if current_details.count(old) > 1:
|
||||
return {"success": False, "error": f"Detail patch {i}: old_string is not unique — add more surrounding context"}
|
||||
current_details = current_details.replace(old, new, 1)
|
||||
details = current_details
|
||||
|
||||
# Update metadata
|
||||
updated_meta = existing_meta.copy()
|
||||
if description is not None:
|
||||
updated_meta["description"] = description
|
||||
if details is not None:
|
||||
updated_meta["details"] = details
|
||||
if metadata:
|
||||
updated_meta.update(metadata)
|
||||
|
||||
|
||||
@@ -197,26 +197,6 @@ def _get_env_yml() -> Optional[Path]:
|
||||
return p if p.exists() else None
|
||||
|
||||
|
||||
def _populate_indicator_types_from_disk(workspace_store, category_manager) -> None:
|
||||
"""Scan existing indicators and add any missing entries to indicator_types store."""
|
||||
existing = workspace_store.read('indicator_types')
|
||||
existing_types = (existing.get('data') or {}).get('types') or {}
|
||||
|
||||
list_result = category_manager.list_items('indicator')
|
||||
items = list_result.get('items', [])
|
||||
added = 0
|
||||
for item in items:
|
||||
item_name = item.get('name', '')
|
||||
if not item_name:
|
||||
continue
|
||||
pandas_ta_name = f"custom_{sanitize_name(item_name).lower()}"
|
||||
if pandas_ta_name not in existing_types:
|
||||
_upsert_indicator_type(workspace_store, category_manager, item_name)
|
||||
added += 1
|
||||
|
||||
if added > 0:
|
||||
logging.info(f"Populated {added} indicator type(s) from disk into indicator_types store")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Configuration
|
||||
@@ -436,6 +416,15 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
|
||||
"type": "string",
|
||||
"description": "LLM-generated description of what this does (required)"
|
||||
},
|
||||
"details": {
|
||||
"type": "string",
|
||||
"description": (
|
||||
"Full markdown description of the code with sufficient detail that another coding agent "
|
||||
"could functionally reproduce the implementation from this field alone. "
|
||||
"Include: purpose, algorithm, all parameters and their semantics, data feed usage, "
|
||||
"formulas, edge cases, and any non-obvious implementation choices (required)."
|
||||
)
|
||||
},
|
||||
"code": {
|
||||
"type": "string",
|
||||
"description": "Python implementation code"
|
||||
@@ -453,7 +442,7 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
|
||||
)
|
||||
}
|
||||
},
|
||||
"required": ["category", "name", "description", "code"]
|
||||
"required": ["category", "name", "description", "details", "code"]
|
||||
}
|
||||
),
|
||||
Tool(
|
||||
@@ -500,6 +489,27 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
|
||||
"type": "string",
|
||||
"description": "Updated description (optional, omit to keep existing)"
|
||||
},
|
||||
"details": {
|
||||
"type": "string",
|
||||
"description": "Full replacement for the details field. Use only when rewriting the entire description; prefer 'detail_patches' for targeted edits."
|
||||
},
|
||||
"detail_patches": {
|
||||
"type": "array",
|
||||
"description": (
|
||||
"Targeted edits to the details field as old/new string pairs. Preferred over 'details' for small changes. "
|
||||
"Each patch: {\"old_string\": \"exact text to find\", \"new_string\": \"replacement text\"}. "
|
||||
"old_string must be unique in the details field (add surrounding context if needed). "
|
||||
"Patches are applied in order. Mutually exclusive with 'details'."
|
||||
),
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"old_string": {"type": "string"},
|
||||
"new_string": {"type": "string"}
|
||||
},
|
||||
"required": ["old_string", "new_string"]
|
||||
}
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object",
|
||||
"description": (
|
||||
@@ -912,6 +922,7 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
|
||||
category=arguments.get("category", ""),
|
||||
name=arguments.get("name", ""),
|
||||
description=arguments.get("description", ""),
|
||||
details=arguments.get("details", ""),
|
||||
code=arguments.get("code", ""),
|
||||
metadata=arguments.get("metadata")
|
||||
)
|
||||
@@ -947,6 +958,8 @@ def create_mcp_server(config: Config, event_publisher: EventPublisher) -> Server
|
||||
code=arguments.get("code"),
|
||||
patches=arguments.get("patches"),
|
||||
description=arguments.get("description"),
|
||||
details=arguments.get("details"),
|
||||
detail_patches=arguments.get("detail_patches"),
|
||||
metadata=arguments.get("metadata")
|
||||
)
|
||||
content = []
|
||||
|
||||
Reference in New Issue
Block a user