backend redesign
This commit is contained in:
349
backend.old/src/indicator/registry.py
Normal file
349
backend.old/src/indicator/registry.py
Normal file
@@ -0,0 +1,349 @@
|
||||
"""
|
||||
Indicator registry for managing and discovering indicators.
|
||||
|
||||
Provides AI agents with a queryable catalog of available indicators,
|
||||
their capabilities, and metadata.
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Optional, Type
|
||||
|
||||
from .base import Indicator
|
||||
from .schema import IndicatorMetadata, InputSchema, OutputSchema
|
||||
|
||||
|
||||
class IndicatorRegistry:
|
||||
"""
|
||||
Central registry for indicator classes.
|
||||
|
||||
Enables:
|
||||
- Registration of indicator implementations
|
||||
- Discovery by name, category, or tags
|
||||
- Schema validation
|
||||
- AI agent tool generation
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._indicators: Dict[str, Type[Indicator]] = {}
|
||||
|
||||
def register(self, indicator_class: Type[Indicator]) -> None:
|
||||
"""
|
||||
Register an indicator class.
|
||||
|
||||
Args:
|
||||
indicator_class: Indicator class to register
|
||||
|
||||
Raises:
|
||||
ValueError: If an indicator with this name is already registered
|
||||
"""
|
||||
metadata = indicator_class.get_metadata()
|
||||
|
||||
if metadata.name in self._indicators:
|
||||
raise ValueError(
|
||||
f"Indicator '{metadata.name}' is already registered"
|
||||
)
|
||||
|
||||
self._indicators[metadata.name] = indicator_class
|
||||
|
||||
def unregister(self, name: str) -> None:
|
||||
"""
|
||||
Unregister an indicator class.
|
||||
|
||||
Args:
|
||||
name: Indicator class name
|
||||
"""
|
||||
self._indicators.pop(name, None)
|
||||
|
||||
def get(self, name: str) -> Optional[Type[Indicator]]:
|
||||
"""
|
||||
Get an indicator class by name.
|
||||
|
||||
Args:
|
||||
name: Indicator class name
|
||||
|
||||
Returns:
|
||||
Indicator class or None if not found
|
||||
"""
|
||||
return self._indicators.get(name)
|
||||
|
||||
def list_indicators(self) -> List[str]:
|
||||
"""
|
||||
Get names of all registered indicators.
|
||||
|
||||
Returns:
|
||||
List of indicator class names
|
||||
"""
|
||||
return list(self._indicators.keys())
|
||||
|
||||
def get_metadata(self, name: str) -> Optional[IndicatorMetadata]:
|
||||
"""
|
||||
Get metadata for a specific indicator.
|
||||
|
||||
Args:
|
||||
name: Indicator class name
|
||||
|
||||
Returns:
|
||||
IndicatorMetadata or None if not found
|
||||
"""
|
||||
indicator_class = self.get(name)
|
||||
if indicator_class:
|
||||
return indicator_class.get_metadata()
|
||||
return None
|
||||
|
||||
def get_all_metadata(self) -> List[IndicatorMetadata]:
|
||||
"""
|
||||
Get metadata for all registered indicators.
|
||||
|
||||
Useful for AI agent tool generation and discovery.
|
||||
|
||||
Returns:
|
||||
List of IndicatorMetadata for all registered indicators
|
||||
"""
|
||||
return [cls.get_metadata() for cls in self._indicators.values()]
|
||||
|
||||
def search_by_category(self, category: str) -> List[IndicatorMetadata]:
|
||||
"""
|
||||
Find indicators by category.
|
||||
|
||||
Args:
|
||||
category: Category name (e.g., 'momentum', 'trend', 'volatility')
|
||||
|
||||
Returns:
|
||||
List of matching indicator metadata
|
||||
"""
|
||||
results = []
|
||||
for indicator_class in self._indicators.values():
|
||||
metadata = indicator_class.get_metadata()
|
||||
if metadata.category.lower() == category.lower():
|
||||
results.append(metadata)
|
||||
return results
|
||||
|
||||
def search_by_tag(self, tag: str) -> List[IndicatorMetadata]:
|
||||
"""
|
||||
Find indicators by tag.
|
||||
|
||||
Args:
|
||||
tag: Tag to search for (case-insensitive)
|
||||
|
||||
Returns:
|
||||
List of matching indicator metadata
|
||||
"""
|
||||
tag_lower = tag.lower()
|
||||
results = []
|
||||
for indicator_class in self._indicators.values():
|
||||
metadata = indicator_class.get_metadata()
|
||||
if any(t.lower() == tag_lower for t in metadata.tags):
|
||||
results.append(metadata)
|
||||
return results
|
||||
|
||||
def search_by_text(self, query: str) -> List[IndicatorMetadata]:
|
||||
"""
|
||||
Full-text search across indicator names, descriptions, and use cases.
|
||||
|
||||
Args:
|
||||
query: Search query (case-insensitive)
|
||||
|
||||
Returns:
|
||||
List of matching indicator metadata, ranked by relevance
|
||||
"""
|
||||
query_lower = query.lower()
|
||||
results = []
|
||||
|
||||
for indicator_class in self._indicators.values():
|
||||
metadata = indicator_class.get_metadata()
|
||||
score = 0
|
||||
|
||||
# Check name (highest weight)
|
||||
if query_lower in metadata.name.lower():
|
||||
score += 10
|
||||
if query_lower in metadata.display_name.lower():
|
||||
score += 8
|
||||
|
||||
# Check description
|
||||
if query_lower in metadata.description.lower():
|
||||
score += 5
|
||||
|
||||
# Check use cases
|
||||
for use_case in metadata.use_cases:
|
||||
if query_lower in use_case.lower():
|
||||
score += 3
|
||||
|
||||
# Check tags
|
||||
for tag in metadata.tags:
|
||||
if query_lower in tag.lower():
|
||||
score += 2
|
||||
|
||||
if score > 0:
|
||||
results.append((score, metadata))
|
||||
|
||||
# Sort by score descending
|
||||
results.sort(key=lambda x: x[0], reverse=True)
|
||||
return [metadata for _, metadata in results]
|
||||
|
||||
def find_compatible_indicators(
|
||||
self,
|
||||
available_columns: List[str],
|
||||
column_types: Dict[str, str]
|
||||
) -> List[IndicatorMetadata]:
|
||||
"""
|
||||
Find indicators that can be computed from available columns.
|
||||
|
||||
Args:
|
||||
available_columns: List of column names available
|
||||
column_types: Mapping of column name to type
|
||||
|
||||
Returns:
|
||||
List of indicators whose input schema is satisfied
|
||||
"""
|
||||
from datasource.schema import ColumnInfo
|
||||
|
||||
# Build ColumnInfo list from available data
|
||||
available_schema = [
|
||||
ColumnInfo(
|
||||
name=name,
|
||||
type=column_types.get(name, "float"),
|
||||
description=f"Column {name}"
|
||||
)
|
||||
for name in available_columns
|
||||
]
|
||||
|
||||
results = []
|
||||
for indicator_class in self._indicators.values():
|
||||
input_schema = indicator_class.get_input_schema()
|
||||
if input_schema.matches(available_schema):
|
||||
results.append(indicator_class.get_metadata())
|
||||
|
||||
return results
|
||||
|
||||
def validate_indicator_chain(
|
||||
self,
|
||||
indicator_chain: List[tuple[str, Dict]]
|
||||
) -> tuple[bool, Optional[str]]:
|
||||
"""
|
||||
Validate that a chain of indicators can be connected.
|
||||
|
||||
Args:
|
||||
indicator_chain: List of (indicator_name, params) tuples in execution order
|
||||
|
||||
Returns:
|
||||
Tuple of (is_valid, error_message)
|
||||
"""
|
||||
if not indicator_chain:
|
||||
return True, None
|
||||
|
||||
# For now, just check that all indicators exist
|
||||
# More sophisticated DAG validation happens in the pipeline engine
|
||||
for indicator_name, params in indicator_chain:
|
||||
if indicator_name not in self._indicators:
|
||||
return False, f"Indicator '{indicator_name}' not found in registry"
|
||||
|
||||
return True, None
|
||||
|
||||
def get_input_schema(self, name: str) -> Optional[InputSchema]:
|
||||
"""
|
||||
Get input schema for a specific indicator.
|
||||
|
||||
Args:
|
||||
name: Indicator class name
|
||||
|
||||
Returns:
|
||||
InputSchema or None if not found
|
||||
"""
|
||||
indicator_class = self.get(name)
|
||||
if indicator_class:
|
||||
return indicator_class.get_input_schema()
|
||||
return None
|
||||
|
||||
def get_output_schema(self, name: str, **params) -> Optional[OutputSchema]:
|
||||
"""
|
||||
Get output schema for a specific indicator with given parameters.
|
||||
|
||||
Args:
|
||||
name: Indicator class name
|
||||
**params: Indicator parameters
|
||||
|
||||
Returns:
|
||||
OutputSchema or None if not found
|
||||
"""
|
||||
indicator_class = self.get(name)
|
||||
if indicator_class:
|
||||
return indicator_class.get_output_schema(**params)
|
||||
return None
|
||||
|
||||
def create_instance(self, name: str, instance_name: str, **params) -> Optional[Indicator]:
|
||||
"""
|
||||
Create an indicator instance with validation.
|
||||
|
||||
Args:
|
||||
name: Indicator class name
|
||||
instance_name: Unique instance name (for output column prefixing)
|
||||
**params: Indicator configuration parameters
|
||||
|
||||
Returns:
|
||||
Indicator instance or None if class not found
|
||||
|
||||
Raises:
|
||||
ValueError: If parameters are invalid
|
||||
"""
|
||||
indicator_class = self.get(name)
|
||||
if not indicator_class:
|
||||
return None
|
||||
|
||||
return indicator_class(instance_name=instance_name, **params)
|
||||
|
||||
def generate_ai_tool_spec(self) -> Dict:
|
||||
"""
|
||||
Generate a JSON specification for AI agent tools.
|
||||
|
||||
Creates a structured representation of all indicators that can be
|
||||
used to build agent tools for indicator selection and composition.
|
||||
|
||||
Returns:
|
||||
Dict suitable for AI agent tool registration
|
||||
"""
|
||||
tools = []
|
||||
|
||||
for indicator_class in self._indicators.values():
|
||||
metadata = indicator_class.get_metadata()
|
||||
|
||||
# Build parameter spec
|
||||
parameters = {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": []
|
||||
}
|
||||
|
||||
for param in metadata.parameters:
|
||||
param_spec = {
|
||||
"type": param.type,
|
||||
"description": param.description
|
||||
}
|
||||
|
||||
if param.default is not None:
|
||||
param_spec["default"] = param.default
|
||||
if param.min_value is not None:
|
||||
param_spec["minimum"] = param.min_value
|
||||
if param.max_value is not None:
|
||||
param_spec["maximum"] = param.max_value
|
||||
|
||||
parameters["properties"][param.name] = param_spec
|
||||
|
||||
if param.required:
|
||||
parameters["required"].append(param.name)
|
||||
|
||||
tool = {
|
||||
"name": f"indicator_{metadata.name.lower()}",
|
||||
"description": f"{metadata.display_name}: {metadata.description}",
|
||||
"category": metadata.category,
|
||||
"use_cases": metadata.use_cases,
|
||||
"tags": metadata.tags,
|
||||
"parameters": parameters,
|
||||
"input_schema": indicator_class.get_input_schema().model_dump(),
|
||||
"output_schema": indicator_class.get_output_schema().model_dump()
|
||||
}
|
||||
|
||||
tools.append(tool)
|
||||
|
||||
return {
|
||||
"indicator_tools": tools,
|
||||
"total_count": len(tools)
|
||||
}
|
||||
Reference in New Issue
Block a user