"""
Behavioral Relationship Strategy
Extracts relationships from the ToolRelationshipGraph instead of type hints.
Used for tool components (component_type="tool") where relationships are
derived from temporal co-occurrence patterns, not structural containment.
This is the counterpart to the existing structural extraction in
RelationshipsMixin._extract_relationships_from_class(), which inspects
class field type hints for containment relationships.
"""
import logging
from typing import Any, Dict, List, Optional, Protocol, runtime_checkable
logger = logging.getLogger(__name__)
# =============================================================================
# RELATIONSHIP STRATEGY PROTOCOL
# =============================================================================
@runtime_checkable
class RelationshipStrategy(Protocol):
"""Base protocol for relationship extraction strategies."""
def extract(self, components: Dict[str, Any]) -> List[Dict[str, Any]]:
"""Extract relationships from a set of components.
Args:
components: Dict mapping full_path -> ModuleComponent
Returns:
List of relationship dictionaries compatible with the pipeline.
"""
...
def get_relationship_text(self, component_name: str) -> str:
"""Generate relationship text for embedding.
Args:
component_name: Name of the component
Returns:
Text suitable for the 'relationships' vector embedding.
"""
...
# =============================================================================
# STRUCTURAL STRATEGY (wrapper around existing logic)
# =============================================================================
class StructuralRelationshipStrategy:
"""Extracts relationships from class field type hints (containment).
This wraps the existing RelationshipsMixin._extract_relationships_from_class()
logic. It's used for component_type="class" components.
The actual extraction is delegated to the mixin, so this class is a thin
adapter that conforms to the RelationshipStrategy protocol.
"""
def __init__(self, mixin_instance: Any):
"""
Args:
mixin_instance: A RelationshipsMixin instance (the ModuleWrapper)
"""
self._mixin = mixin_instance
def extract(self, components: Dict[str, Any]) -> List[Dict[str, Any]]:
"""Delegates to RelationshipsMixin.extract_relationships()."""
return self._mixin.extract_relationships()
def get_relationship_text(self, component_name: str) -> str:
"""Build compact relationship text from structural relationships."""
from adapters.module_wrapper.pipeline_mixin import (
build_compact_relationship_text,
)
by_parent = self._mixin.extract_relationships_by_parent()
rels = by_parent.get(component_name, [])
comp = None
for path, c in self._mixin.components.items():
if c.name == component_name:
comp = c
break
comp_type = comp.component_type if comp else "class"
return build_compact_relationship_text(component_name, rels, comp_type)
# =============================================================================
# BEHAVIORAL STRATEGY (graph-based)
# =============================================================================
class BehavioralRelationshipStrategy:
"""Extracts relationships from ToolRelationshipGraph co-occurrence patterns.
Used for component_type="tool" components where relationships are temporal
(tool A called before/after tool B) rather than structural (class A contains field B).
Produces relationship dicts compatible with the existing pipeline:
{
"parent_class": tool_name,
"child_class": successor_tool_name,
"field_name": "successor",
"is_optional": True,
"depth": 1,
"relationship_path": "tool.successor",
"json_path": "tool.successor",
"nl_description": "send_message frequently followed by list_messages",
}
"""
def __init__(self, tool_graph: Any):
"""
Args:
tool_graph: A ToolRelationshipGraph instance
"""
from adapters.module_wrapper.tool_relationship_graph import (
ToolRelationshipGraph,
)
self._graph: ToolRelationshipGraph = tool_graph
def extract(self, components: Dict[str, Any]) -> List[Dict[str, Any]]:
"""Extract behavioral relationships for all tool components.
Returns relationship dicts where:
- "parent_class" = the tool
- "child_class" = a frequent successor tool
- "field_name" = "successor" (behavioral, not structural)
"""
relationships = []
for full_path, component in components.items():
if getattr(component, "component_type", "") != "tool":
continue
tool_name = component.name
successors = self._graph.get_successors(tool_name, top_n=5)
for succ_name, count in successors:
node_info = self._graph.get_node_info(tool_name)
succ_info = self._graph.get_node_info(succ_name)
service = node_info["service"] if node_info else ""
succ_service = succ_info["service"] if succ_info else ""
nl_desc = f"{tool_name} frequently followed by {succ_name}"
if service and succ_service and service != succ_service:
nl_desc += f" (cross-service: {service} -> {succ_service})"
elif service:
nl_desc += f" (within {service})"
relationships.append(
{
"parent_class": tool_name,
"parent_module": f"tools.{service}" if service else "tools",
"parent_path": f"tools.{service}.{tool_name}"
if service
else f"tools.{tool_name}",
"child_class": succ_name,
"child_module": f"tools.{succ_service}"
if succ_service
else "tools",
"child_path": f"tools.{succ_service}.{succ_name}"
if succ_service
else f"tools.{succ_name}",
"field_name": "successor",
"is_optional": True,
"depth": 1,
"relationship_path": f"{tool_name}.successor.{succ_name}",
"json_path": f"{tool_name}.successor.{succ_name}",
"nl_description": nl_desc,
"co_occurrence_count": count,
"relationship_type": "behavioral",
}
)
return relationships
def get_relationship_text(
self,
tool_name: str,
user_email: str = "",
session_id: str = "",
) -> str:
"""Generate relationship text from the tool co-occurrence graph.
This is the text that gets embedded into the 'relationships' vector.
"""
return self._graph.get_relationship_text(
tool_name,
user_email=user_email,
session_id=session_id,
)
__all__ = [
"RelationshipStrategy",
"StructuralRelationshipStrategy",
"BehavioralRelationshipStrategy",
]