flock-core 0.4.520__py3-none-any.whl → 0.5.0b2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of flock-core might be problematic. Click here for more details.
- flock/cli/manage_agents.py +3 -3
- flock/components/__init__.py +28 -0
- flock/components/evaluation/__init__.py +9 -0
- flock/components/evaluation/declarative_evaluation_component.py +198 -0
- flock/components/routing/__init__.py +15 -0
- flock/{routers/conditional/conditional_router.py → components/routing/conditional_routing_component.py} +60 -49
- flock/components/routing/default_routing_component.py +103 -0
- flock/components/routing/llm_routing_component.py +208 -0
- flock/components/utility/__init__.py +15 -0
- flock/{modules/enterprise_memory/enterprise_memory_module.py → components/utility/memory_utility_component.py} +195 -173
- flock/{modules/performance/metrics_module.py → components/utility/metrics_utility_component.py} +101 -86
- flock/{modules/output/output_module.py → components/utility/output_utility_component.py} +49 -49
- flock/core/__init__.py +2 -8
- flock/core/agent/__init__.py +16 -0
- flock/core/agent/flock_agent_components.py +104 -0
- flock/core/agent/flock_agent_execution.py +101 -0
- flock/core/agent/flock_agent_integration.py +147 -0
- flock/core/agent/flock_agent_lifecycle.py +177 -0
- flock/core/agent/flock_agent_serialization.py +378 -0
- flock/core/component/__init__.py +15 -0
- flock/core/{flock_module.py → component/agent_component_base.py} +136 -35
- flock/core/component/evaluation_component_base.py +56 -0
- flock/core/component/routing_component_base.py +75 -0
- flock/core/component/utility_component_base.py +69 -0
- flock/core/config/flock_agent_config.py +49 -2
- flock/core/evaluation/utils.py +1 -1
- flock/core/execution/evaluation_executor.py +1 -1
- flock/core/flock.py +137 -483
- flock/core/flock_agent.py +151 -1018
- flock/core/flock_factory.py +94 -73
- flock/core/{flock_registry.py → flock_registry.py.backup} +3 -17
- flock/core/logging/logging.py +1 -0
- flock/core/mcp/flock_mcp_server.py +42 -37
- flock/core/mixin/dspy_integration.py +5 -5
- flock/core/orchestration/__init__.py +18 -0
- flock/core/orchestration/flock_batch_processor.py +94 -0
- flock/core/orchestration/flock_evaluator.py +113 -0
- flock/core/orchestration/flock_execution.py +288 -0
- flock/core/orchestration/flock_initialization.py +125 -0
- flock/core/orchestration/flock_server_manager.py +65 -0
- flock/core/orchestration/flock_web_server.py +117 -0
- flock/core/registry/__init__.py +39 -0
- flock/core/registry/agent_registry.py +69 -0
- flock/core/registry/callable_registry.py +139 -0
- flock/core/registry/component_discovery.py +142 -0
- flock/core/registry/component_registry.py +64 -0
- flock/core/registry/config_mapping.py +64 -0
- flock/core/registry/decorators.py +137 -0
- flock/core/registry/registry_hub.py +202 -0
- flock/core/registry/server_registry.py +57 -0
- flock/core/registry/type_registry.py +86 -0
- flock/core/serialization/flock_serializer.py +33 -30
- flock/core/serialization/serialization_utils.py +28 -25
- flock/core/util/input_resolver.py +29 -2
- flock/platform/docker_tools.py +3 -3
- flock/tools/markdown_tools.py +1 -2
- flock/tools/text_tools.py +1 -2
- flock/webapp/app/main.py +9 -5
- flock/workflow/activities.py +59 -84
- flock/workflow/activities_unified.py +230 -0
- flock/workflow/agent_execution_activity.py +6 -6
- flock/workflow/flock_workflow.py +1 -1
- {flock_core-0.4.520.dist-info → flock_core-0.5.0b2.dist-info}/METADATA +2 -2
- {flock_core-0.4.520.dist-info → flock_core-0.5.0b2.dist-info}/RECORD +67 -68
- flock/core/flock_evaluator.py +0 -60
- flock/core/flock_router.py +0 -83
- flock/evaluators/__init__.py +0 -1
- flock/evaluators/declarative/__init__.py +0 -1
- flock/evaluators/declarative/declarative_evaluator.py +0 -194
- flock/evaluators/memory/memory_evaluator.py +0 -90
- flock/evaluators/test/test_case_evaluator.py +0 -38
- flock/evaluators/zep/zep_evaluator.py +0 -59
- flock/modules/__init__.py +0 -1
- flock/modules/assertion/__init__.py +0 -1
- flock/modules/assertion/assertion_module.py +0 -286
- flock/modules/callback/__init__.py +0 -1
- flock/modules/callback/callback_module.py +0 -91
- flock/modules/enterprise_memory/README.md +0 -99
- flock/modules/mem0/__init__.py +0 -1
- flock/modules/mem0/mem0_module.py +0 -126
- flock/modules/mem0_async/__init__.py +0 -1
- flock/modules/mem0_async/async_mem0_module.py +0 -126
- flock/modules/memory/__init__.py +0 -1
- flock/modules/memory/memory_module.py +0 -429
- flock/modules/memory/memory_parser.py +0 -125
- flock/modules/memory/memory_storage.py +0 -736
- flock/modules/output/__init__.py +0 -1
- flock/modules/performance/__init__.py +0 -1
- flock/modules/zep/__init__.py +0 -1
- flock/modules/zep/zep_module.py +0 -192
- flock/routers/__init__.py +0 -1
- flock/routers/agent/__init__.py +0 -1
- flock/routers/agent/agent_router.py +0 -236
- flock/routers/agent/handoff_agent.py +0 -58
- flock/routers/default/__init__.py +0 -1
- flock/routers/default/default_router.py +0 -80
- flock/routers/feedback/feedback_router.py +0 -114
- flock/routers/list_generator/list_generator_router.py +0 -166
- flock/routers/llm/__init__.py +0 -1
- flock/routers/llm/llm_router.py +0 -365
- {flock_core-0.4.520.dist-info → flock_core-0.5.0b2.dist-info}/WHEEL +0 -0
- {flock_core-0.4.520.dist-info → flock_core-0.5.0b2.dist-info}/entry_points.txt +0 -0
- {flock_core-0.4.520.dist-info → flock_core-0.5.0b2.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# src/flock/core/orchestration/flock_web_server.py
|
|
2
|
+
"""Web server and CLI management functionality for Flock orchestrator."""
|
|
3
|
+
|
|
4
|
+
from collections.abc import Callable, Sequence
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
from flock.core.logging.logging import get_logger
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from flock.core.api.custom_endpoint import FlockEndpoint
|
|
11
|
+
from flock.core.flock import Flock
|
|
12
|
+
from flock.core.flock_agent import FlockAgent
|
|
13
|
+
|
|
14
|
+
logger = get_logger("flock.web_server")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class FlockWebServer:
|
|
18
|
+
"""Handles web server and CLI functionality for Flock orchestrator."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, flock: "Flock"):
|
|
21
|
+
self.flock = flock
|
|
22
|
+
|
|
23
|
+
def serve(
|
|
24
|
+
self,
|
|
25
|
+
host: str = "127.0.0.1",
|
|
26
|
+
port: int = 8344,
|
|
27
|
+
server_name: str = "Flock Server",
|
|
28
|
+
ui: bool = True,
|
|
29
|
+
chat: bool = False,
|
|
30
|
+
chat_agent: str | None = None,
|
|
31
|
+
chat_message_key: str = "message",
|
|
32
|
+
chat_history_key: str = "history",
|
|
33
|
+
chat_response_key: str = "response",
|
|
34
|
+
ui_theme: str | None = None,
|
|
35
|
+
custom_endpoints: Sequence["FlockEndpoint"]
|
|
36
|
+
| dict[tuple[str, list[str] | None], Callable[..., Any]]
|
|
37
|
+
| None = None,
|
|
38
|
+
) -> None:
|
|
39
|
+
"""Launch an HTTP server that exposes the core REST API and, optionally, the browser-based UI."""
|
|
40
|
+
try:
|
|
41
|
+
from flock.webapp.run import start_unified_server
|
|
42
|
+
except ImportError:
|
|
43
|
+
logger.error(
|
|
44
|
+
"Web application components not found (flock.webapp.run). "
|
|
45
|
+
"Cannot start HTTP server. Ensure webapp dependencies are installed."
|
|
46
|
+
)
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
logger.info(
|
|
50
|
+
f"Attempting to start server for Flock '{self.flock.name}' on {host}:{port}. UI enabled: {ui}"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
start_unified_server(
|
|
54
|
+
flock_instance=self.flock,
|
|
55
|
+
host=host,
|
|
56
|
+
port=port,
|
|
57
|
+
server_title=server_name,
|
|
58
|
+
enable_ui_routes=ui,
|
|
59
|
+
enable_chat_routes=chat,
|
|
60
|
+
ui_theme=ui_theme,
|
|
61
|
+
custom_endpoints=custom_endpoints,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
def start_api(
|
|
65
|
+
self,
|
|
66
|
+
host: str = "127.0.0.1",
|
|
67
|
+
port: int = 8344,
|
|
68
|
+
server_name: str = "Flock Server",
|
|
69
|
+
create_ui: bool = True,
|
|
70
|
+
ui_theme: str | None = None,
|
|
71
|
+
custom_endpoints: Sequence["FlockEndpoint"]
|
|
72
|
+
| dict[tuple[str, list[str] | None], Callable[..., Any]]
|
|
73
|
+
| None = None,
|
|
74
|
+
) -> None:
|
|
75
|
+
"""Deprecated: Use serve() instead."""
|
|
76
|
+
import warnings
|
|
77
|
+
|
|
78
|
+
warnings.warn(
|
|
79
|
+
"start_api() is deprecated and will be removed in a future release. "
|
|
80
|
+
"Use serve() instead.",
|
|
81
|
+
DeprecationWarning,
|
|
82
|
+
stacklevel=2,
|
|
83
|
+
)
|
|
84
|
+
# Delegate to the new serve() method (create_ui maps to ui)
|
|
85
|
+
return self.serve(
|
|
86
|
+
host=host,
|
|
87
|
+
port=port,
|
|
88
|
+
server_name=server_name,
|
|
89
|
+
ui=create_ui,
|
|
90
|
+
ui_theme=ui_theme,
|
|
91
|
+
custom_endpoints=custom_endpoints,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
def start_cli(
|
|
95
|
+
self,
|
|
96
|
+
start_agent: "FlockAgent | str | None" = None,
|
|
97
|
+
server_name: str = "Flock CLI",
|
|
98
|
+
show_results: bool = False,
|
|
99
|
+
edit_mode: bool = False,
|
|
100
|
+
) -> None:
|
|
101
|
+
"""Starts an interactive CLI for this Flock instance."""
|
|
102
|
+
try:
|
|
103
|
+
from flock.cli.runner import start_flock_cli
|
|
104
|
+
except ImportError:
|
|
105
|
+
logger.error(
|
|
106
|
+
"CLI components not found. Cannot start CLI. "
|
|
107
|
+
"Ensure CLI dependencies are installed."
|
|
108
|
+
)
|
|
109
|
+
return
|
|
110
|
+
|
|
111
|
+
logger.info(f"Starting CLI for Flock '{self.flock.name}'...")
|
|
112
|
+
start_flock_cli(
|
|
113
|
+
flock=self.flock, # Pass the Flock instance
|
|
114
|
+
server_name=server_name,
|
|
115
|
+
show_results=show_results,
|
|
116
|
+
edit_mode=edit_mode,
|
|
117
|
+
)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# src/flock/core/registry/__init__.py
|
|
2
|
+
"""Modern thread-safe registry system using composition pattern.
|
|
3
|
+
|
|
4
|
+
This module provides a complete refactor of the FlockRegistry using
|
|
5
|
+
the proven composition pattern from Flock and FlockAgent refactoring.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from flock.core.registry.registry_hub import RegistryHub, get_registry
|
|
9
|
+
from flock.core.registry.decorators import flock_component, flock_tool, flock_callable, flock_type
|
|
10
|
+
|
|
11
|
+
# Specialized registry components (for advanced usage)
|
|
12
|
+
from flock.core.registry.component_registry import ComponentRegistry
|
|
13
|
+
from flock.core.registry.callable_registry import CallableRegistry
|
|
14
|
+
from flock.core.registry.agent_registry import AgentRegistry
|
|
15
|
+
from flock.core.registry.server_registry import ServerRegistry
|
|
16
|
+
from flock.core.registry.type_registry import TypeRegistry
|
|
17
|
+
from flock.core.registry.config_mapping import ConfigMapping
|
|
18
|
+
from flock.core.registry.component_discovery import ComponentDiscovery
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
# Main API
|
|
22
|
+
"RegistryHub",
|
|
23
|
+
"get_registry",
|
|
24
|
+
|
|
25
|
+
# Decorators
|
|
26
|
+
"flock_component",
|
|
27
|
+
"flock_tool",
|
|
28
|
+
"flock_callable",
|
|
29
|
+
"flock_type",
|
|
30
|
+
|
|
31
|
+
# Specialized registries (for advanced usage)
|
|
32
|
+
"ComponentRegistry",
|
|
33
|
+
"CallableRegistry",
|
|
34
|
+
"AgentRegistry",
|
|
35
|
+
"ServerRegistry",
|
|
36
|
+
"TypeRegistry",
|
|
37
|
+
"ConfigMapping",
|
|
38
|
+
"ComponentDiscovery",
|
|
39
|
+
]
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# src/flock/core/registry/agent_registry.py
|
|
2
|
+
"""Agent instance registration and lookup functionality."""
|
|
3
|
+
|
|
4
|
+
import threading
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from flock.core.logging.logging import get_logger
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from flock.core.flock_agent import FlockAgent
|
|
11
|
+
|
|
12
|
+
logger = get_logger("registry.agents")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AgentRegistry:
|
|
16
|
+
"""Manages FlockAgent instance registration and lookup with thread safety."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, lock: threading.RLock):
|
|
19
|
+
self._lock = lock
|
|
20
|
+
self._agents: dict[str, "FlockAgent"] = {}
|
|
21
|
+
|
|
22
|
+
def register_agent(self, agent: "FlockAgent", *, force: bool = False) -> None:
|
|
23
|
+
"""Register a FlockAgent instance by its name.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
agent: The agent instance to register.
|
|
27
|
+
force: If True, allow overwriting an existing **different** agent registered under the same name.
|
|
28
|
+
If False and a conflicting registration exists, a ValueError is raised.
|
|
29
|
+
"""
|
|
30
|
+
if not hasattr(agent, "name") or not agent.name:
|
|
31
|
+
logger.error("Attempted to register an agent without a valid 'name' attribute.")
|
|
32
|
+
return
|
|
33
|
+
|
|
34
|
+
with self._lock:
|
|
35
|
+
if agent.name in self._agents and self._agents[agent.name] is not agent:
|
|
36
|
+
# Same agent already registered → silently ignore; different instance → error/force.
|
|
37
|
+
if not force:
|
|
38
|
+
raise ValueError(
|
|
39
|
+
f"Agent '{agent.name}' already registered with a different instance. "
|
|
40
|
+
"Pass force=True to overwrite the existing registration."
|
|
41
|
+
)
|
|
42
|
+
logger.warning(f"Overwriting existing agent '{agent.name}' registration due to force=True.")
|
|
43
|
+
|
|
44
|
+
self._agents[agent.name] = agent
|
|
45
|
+
logger.debug(f"Registered agent: {agent.name}")
|
|
46
|
+
|
|
47
|
+
def get_agent(self, name: str) -> "FlockAgent | None":
|
|
48
|
+
"""Retrieve a registered FlockAgent instance by name."""
|
|
49
|
+
with self._lock:
|
|
50
|
+
agent = self._agents.get(name)
|
|
51
|
+
if not agent:
|
|
52
|
+
logger.warning(f"Agent '{name}' not found in registry.")
|
|
53
|
+
return agent
|
|
54
|
+
|
|
55
|
+
def get_all_agent_names(self) -> list[str]:
|
|
56
|
+
"""Return a list of names of all registered agents."""
|
|
57
|
+
with self._lock:
|
|
58
|
+
return list(self._agents.keys())
|
|
59
|
+
|
|
60
|
+
def get_all_agents(self) -> dict[str, "FlockAgent"]:
|
|
61
|
+
"""Get all registered agents."""
|
|
62
|
+
with self._lock:
|
|
63
|
+
return self._agents.copy()
|
|
64
|
+
|
|
65
|
+
def clear(self) -> None:
|
|
66
|
+
"""Clear all registered agents."""
|
|
67
|
+
with self._lock:
|
|
68
|
+
self._agents.clear()
|
|
69
|
+
logger.debug("Cleared all registered agents")
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# src/flock/core/registry/callable_registry.py
|
|
2
|
+
"""Callable function/method registration and lookup functionality."""
|
|
3
|
+
|
|
4
|
+
import builtins
|
|
5
|
+
import importlib
|
|
6
|
+
import threading
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from flock.core.logging.logging import get_logger
|
|
11
|
+
|
|
12
|
+
logger = get_logger("registry.callables")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CallableRegistry:
|
|
16
|
+
"""Manages callable registration with smart lookup and thread safety."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, lock: threading.RLock):
|
|
19
|
+
self._lock = lock
|
|
20
|
+
self._callables: dict[str, Callable] = {}
|
|
21
|
+
|
|
22
|
+
def register_callable(self, func: Callable, name: str | None = None) -> str | None:
|
|
23
|
+
"""Register a callable (function/method). Returns its path string identifier."""
|
|
24
|
+
path_str = name or self._get_path_string(func)
|
|
25
|
+
if not path_str:
|
|
26
|
+
logger.warning(f"Could not register callable {getattr(func, '__name__', 'unknown')}: Unable to determine path string")
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
with self._lock:
|
|
30
|
+
if path_str in self._callables and self._callables[path_str] != func:
|
|
31
|
+
logger.warning(f"Callable '{path_str}' already registered with a different function. Overwriting.")
|
|
32
|
+
|
|
33
|
+
self._callables[path_str] = func
|
|
34
|
+
logger.debug(f"Registered callable: '{path_str}' ({getattr(func, '__name__', 'unknown')})")
|
|
35
|
+
return path_str
|
|
36
|
+
|
|
37
|
+
def get_callable(self, name_or_path: str) -> Callable:
|
|
38
|
+
"""Retrieve a callable by its registered name or full path string.
|
|
39
|
+
|
|
40
|
+
Attempts dynamic import if not found directly. Prioritizes exact match,
|
|
41
|
+
then searches for matches ending with '.{name}'.
|
|
42
|
+
"""
|
|
43
|
+
# 1. Try exact match first (covers full paths and simple names if registered that way)
|
|
44
|
+
with self._lock:
|
|
45
|
+
if name_or_path in self._callables:
|
|
46
|
+
logger.debug(f"Found callable '{name_or_path}' directly in registry.")
|
|
47
|
+
return self._callables[name_or_path]
|
|
48
|
+
|
|
49
|
+
# 2. If not found, and it looks like a simple name, search registered paths
|
|
50
|
+
if "." not in name_or_path:
|
|
51
|
+
with self._lock:
|
|
52
|
+
matches = []
|
|
53
|
+
for path_str, func in self._callables.items():
|
|
54
|
+
# Check if path ends with ".{simple_name}" or exactly matches simple_name
|
|
55
|
+
if path_str == name_or_path or path_str.endswith(f".{name_or_path}"):
|
|
56
|
+
matches.append(func)
|
|
57
|
+
|
|
58
|
+
if len(matches) == 1:
|
|
59
|
+
logger.debug(f"Found unique callable for simple name '{name_or_path}' via path '{self.get_callable_path_string(matches[0])}'.")
|
|
60
|
+
return matches[0]
|
|
61
|
+
elif len(matches) > 1:
|
|
62
|
+
# Ambiguous simple name - require full path
|
|
63
|
+
found_paths = [self.get_callable_path_string(f) for f in matches]
|
|
64
|
+
logger.error(f"Ambiguous callable name '{name_or_path}'. Found matches: {found_paths}. Use full path string for lookup.")
|
|
65
|
+
raise KeyError(f"Ambiguous callable name '{name_or_path}'. Use full path string.")
|
|
66
|
+
|
|
67
|
+
# 3. Attempt dynamic import if it looks like a full path
|
|
68
|
+
if "." in name_or_path:
|
|
69
|
+
logger.debug(f"Callable '{name_or_path}' not in registry cache, attempting dynamic import.")
|
|
70
|
+
try:
|
|
71
|
+
module_name, func_name = name_or_path.rsplit(".", 1)
|
|
72
|
+
module = importlib.import_module(module_name)
|
|
73
|
+
func = getattr(module, func_name)
|
|
74
|
+
if callable(func):
|
|
75
|
+
self.register_callable(func, name_or_path) # Cache dynamically imported
|
|
76
|
+
logger.info(f"Successfully imported and registered module callable '{name_or_path}'")
|
|
77
|
+
return func
|
|
78
|
+
else:
|
|
79
|
+
raise TypeError(f"Dynamically imported object '{name_or_path}' is not callable.")
|
|
80
|
+
except (ImportError, AttributeError, TypeError) as e:
|
|
81
|
+
logger.error(f"Failed to dynamically load/find callable '{name_or_path}': {e}", exc_info=False)
|
|
82
|
+
|
|
83
|
+
# 4. Handle built-ins if not found yet (might be redundant if simple name check worked)
|
|
84
|
+
elif name_or_path in builtins.__dict__:
|
|
85
|
+
func = builtins.__dict__[name_or_path]
|
|
86
|
+
if callable(func):
|
|
87
|
+
self.register_callable(func, name_or_path) # Cache it
|
|
88
|
+
logger.info(f"Found and registered built-in callable '{name_or_path}'")
|
|
89
|
+
return func
|
|
90
|
+
|
|
91
|
+
# 5. Final failure
|
|
92
|
+
logger.error(f"Callable '{name_or_path}' not found in registry or via import.")
|
|
93
|
+
raise KeyError(f"Callable '{name_or_path}' not found.")
|
|
94
|
+
|
|
95
|
+
def get_callable_path_string(self, func: Callable) -> str | None:
|
|
96
|
+
"""Get the path string for a callable, registering it if necessary."""
|
|
97
|
+
# First try to find by direct identity
|
|
98
|
+
with self._lock:
|
|
99
|
+
for path_str, registered_func in self._callables.items():
|
|
100
|
+
if func == registered_func:
|
|
101
|
+
logger.debug(f"Found existing path string for callable: '{path_str}'")
|
|
102
|
+
return path_str
|
|
103
|
+
|
|
104
|
+
# If not found by identity, generate path, register, and return
|
|
105
|
+
path_str = self.register_callable(func)
|
|
106
|
+
if path_str:
|
|
107
|
+
logger.debug(f"Generated and registered new path string for callable: '{path_str}'")
|
|
108
|
+
else:
|
|
109
|
+
logger.warning(f"Failed to generate path string for callable {getattr(func, '__name__', 'unknown')}")
|
|
110
|
+
|
|
111
|
+
return path_str
|
|
112
|
+
|
|
113
|
+
def get_all_callables(self) -> dict[str, Callable]:
|
|
114
|
+
"""Get all registered callables."""
|
|
115
|
+
with self._lock:
|
|
116
|
+
return self._callables.copy()
|
|
117
|
+
|
|
118
|
+
def clear(self) -> None:
|
|
119
|
+
"""Clear all registered callables."""
|
|
120
|
+
with self._lock:
|
|
121
|
+
self._callables.clear()
|
|
122
|
+
logger.debug("Cleared all registered callables")
|
|
123
|
+
|
|
124
|
+
@staticmethod
|
|
125
|
+
def _get_path_string(obj: Callable | type) -> str | None:
|
|
126
|
+
"""Generate a unique path string 'module.ClassName' or 'module.function_name'."""
|
|
127
|
+
try:
|
|
128
|
+
module = obj.__module__
|
|
129
|
+
name = obj.__name__
|
|
130
|
+
if module == "builtins":
|
|
131
|
+
return name
|
|
132
|
+
# Check if it's nested (basic check, might not cover all edge cases)
|
|
133
|
+
if "." in name and hasattr(__import__(module).__dict__, name.split(".")[0]):
|
|
134
|
+
# Likely a nested class/method - serialization might need custom handling or pickle
|
|
135
|
+
logger.warning(f"Object {name} appears nested in {module}. Path string might be ambiguous.")
|
|
136
|
+
return f"{module}.{name}"
|
|
137
|
+
except AttributeError:
|
|
138
|
+
logger.warning(f"Could not determine module/name for object: {obj}")
|
|
139
|
+
return None
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# src/flock/core/registry/component_discovery.py
|
|
2
|
+
"""Component discovery and auto-registration functionality."""
|
|
3
|
+
|
|
4
|
+
import importlib
|
|
5
|
+
import importlib.util
|
|
6
|
+
import inspect
|
|
7
|
+
import os
|
|
8
|
+
import pkgutil
|
|
9
|
+
import threading
|
|
10
|
+
from dataclasses import is_dataclass
|
|
11
|
+
from typing import TYPE_CHECKING, Any
|
|
12
|
+
|
|
13
|
+
from pydantic import BaseModel
|
|
14
|
+
from flock.core.logging.logging import get_logger
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from flock.core.registry.registry_hub import RegistryHub
|
|
18
|
+
from flock.core.component.agent_component_base import AgentComponent
|
|
19
|
+
|
|
20
|
+
logger = get_logger("registry.discovery")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ComponentDiscovery:
|
|
24
|
+
"""Handles automatic component discovery and registration."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, registry_hub: "RegistryHub"):
|
|
27
|
+
self.registry_hub = registry_hub
|
|
28
|
+
self._packages_to_scan = [
|
|
29
|
+
"flock.tools",
|
|
30
|
+
"flock.components", # Updated to use unified components
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
def discover_and_register_components(self) -> None:
|
|
34
|
+
"""Auto-register components from known core packages."""
|
|
35
|
+
for package_name in self._packages_to_scan:
|
|
36
|
+
try:
|
|
37
|
+
package_spec = importlib.util.find_spec(package_name)
|
|
38
|
+
if package_spec and package_spec.origin:
|
|
39
|
+
package_path_list = [os.path.dirname(package_spec.origin)]
|
|
40
|
+
logger.info(f"Recursively scanning for modules in package: {package_name} (path: {package_path_list[0]})")
|
|
41
|
+
|
|
42
|
+
# Use walk_packages to recursively find all modules
|
|
43
|
+
for module_loader, module_name, is_pkg in pkgutil.walk_packages(
|
|
44
|
+
path=package_path_list,
|
|
45
|
+
prefix=package_name + ".", # Ensures module_name is fully qualified
|
|
46
|
+
onerror=lambda name: logger.warning(f"Error importing module {name} during scan.")
|
|
47
|
+
):
|
|
48
|
+
if not is_pkg and not module_name.split('.')[-1].startswith("_"):
|
|
49
|
+
# We are interested in actual modules, not sub-packages themselves for registration
|
|
50
|
+
# And also skip modules starting with underscore (e.g. __main__.py)
|
|
51
|
+
try:
|
|
52
|
+
logger.debug(f"Attempting to auto-register components from module: {module_name}")
|
|
53
|
+
self.register_module_components(module_name)
|
|
54
|
+
except ImportError as e:
|
|
55
|
+
logger.warning(
|
|
56
|
+
f"Could not auto-register from {module_name}: Module not found or import error: {e}"
|
|
57
|
+
)
|
|
58
|
+
except Exception as e: # Catch other potential errors during registration
|
|
59
|
+
logger.error(
|
|
60
|
+
f"Unexpected error during auto-registration of {module_name}: {e}",
|
|
61
|
+
exc_info=True
|
|
62
|
+
)
|
|
63
|
+
else:
|
|
64
|
+
logger.warning(f"Could not find package spec for '{package_name}' to auto-register components/tools.")
|
|
65
|
+
except Exception as e:
|
|
66
|
+
logger.error(f"Error while trying to dynamically register from '{package_name}': {e}", exc_info=True)
|
|
67
|
+
|
|
68
|
+
def register_module_components(self, module_or_path: Any) -> None:
|
|
69
|
+
"""Scan a module (object or path string) and automatically register.
|
|
70
|
+
|
|
71
|
+
- Functions as callables.
|
|
72
|
+
- Pydantic Models and Dataclasses as types.
|
|
73
|
+
- Subclasses of AgentComponent as components.
|
|
74
|
+
"""
|
|
75
|
+
try:
|
|
76
|
+
if isinstance(module_or_path, str):
|
|
77
|
+
module = importlib.import_module(module_or_path)
|
|
78
|
+
elif inspect.ismodule(module_or_path):
|
|
79
|
+
module = module_or_path
|
|
80
|
+
else:
|
|
81
|
+
logger.error(
|
|
82
|
+
f"Invalid input for auto-registration: {module_or_path}. Must be module object or path string."
|
|
83
|
+
)
|
|
84
|
+
return
|
|
85
|
+
|
|
86
|
+
logger.info(f"Auto-registering components from module: {module.__name__}")
|
|
87
|
+
registered_count = {"callable": 0, "type": 0, "component": 0}
|
|
88
|
+
|
|
89
|
+
for name, obj in inspect.getmembers(module):
|
|
90
|
+
if name.startswith("_"):
|
|
91
|
+
continue # Skip private/internal
|
|
92
|
+
|
|
93
|
+
# Register Functions as Callables
|
|
94
|
+
if (
|
|
95
|
+
inspect.isfunction(obj)
|
|
96
|
+
and obj.__module__ == module.__name__
|
|
97
|
+
):
|
|
98
|
+
if self.registry_hub.callables.register_callable(obj):
|
|
99
|
+
registered_count["callable"] += 1
|
|
100
|
+
|
|
101
|
+
# Register Classes (Types and Components)
|
|
102
|
+
elif inspect.isclass(obj) and obj.__module__ == module.__name__:
|
|
103
|
+
is_component = False
|
|
104
|
+
|
|
105
|
+
# Register as Component if subclass of AgentComponent
|
|
106
|
+
try:
|
|
107
|
+
from flock.core.component.agent_component_base import AgentComponent
|
|
108
|
+
if (
|
|
109
|
+
issubclass(obj, AgentComponent)
|
|
110
|
+
and self.registry_hub.components.register_component(obj)
|
|
111
|
+
):
|
|
112
|
+
registered_count["component"] += 1
|
|
113
|
+
is_component = True # Mark as component
|
|
114
|
+
except ImportError:
|
|
115
|
+
# AgentComponent not available during setup
|
|
116
|
+
pass
|
|
117
|
+
|
|
118
|
+
# Register as Type if Pydantic Model or Dataclass
|
|
119
|
+
# A component can also be a type used in signatures
|
|
120
|
+
base_model_or_dataclass = isinstance(obj, type) and (
|
|
121
|
+
issubclass(obj, BaseModel) or is_dataclass(obj)
|
|
122
|
+
)
|
|
123
|
+
if (
|
|
124
|
+
base_model_or_dataclass
|
|
125
|
+
and self.registry_hub.types.register_type(obj)
|
|
126
|
+
and not is_component
|
|
127
|
+
):
|
|
128
|
+
# Only increment type count if it wasn't already counted as component
|
|
129
|
+
registered_count["type"] += 1
|
|
130
|
+
|
|
131
|
+
logger.info(
|
|
132
|
+
f"Auto-registration summary for {module.__name__}: "
|
|
133
|
+
f"{registered_count['callable']} callables, "
|
|
134
|
+
f"{registered_count['type']} types, "
|
|
135
|
+
f"{registered_count['component']} components."
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
except Exception as e:
|
|
139
|
+
logger.error(
|
|
140
|
+
f"Error during auto-registration for {module_or_path}: {e}",
|
|
141
|
+
exc_info=True,
|
|
142
|
+
)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# src/flock/core/registry/component_registry.py
|
|
2
|
+
"""Component class registration and lookup functionality."""
|
|
3
|
+
|
|
4
|
+
import threading
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
from flock.core.logging.logging import get_logger
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from flock.core.component.agent_component_base import AgentComponent
|
|
11
|
+
|
|
12
|
+
logger = get_logger("registry.components")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ComponentRegistry:
|
|
16
|
+
"""Manages component class registration and lookup with thread safety."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, lock: threading.RLock):
|
|
19
|
+
self._lock = lock
|
|
20
|
+
self._components: dict[str, type] = {}
|
|
21
|
+
|
|
22
|
+
def register_component(self, component_class: type, name: str | None = None) -> str | None:
|
|
23
|
+
"""Register a component class (evaluation, routing, utility components)."""
|
|
24
|
+
type_name = name or component_class.__name__
|
|
25
|
+
if not type_name:
|
|
26
|
+
logger.error(f"Could not determine name for component class: {component_class}")
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
with self._lock:
|
|
30
|
+
if type_name in self._components and self._components[type_name] != component_class:
|
|
31
|
+
logger.warning(f"Component class '{type_name}' already registered. Overwriting.")
|
|
32
|
+
|
|
33
|
+
self._components[type_name] = component_class
|
|
34
|
+
logger.debug(f"Registered component class: {type_name}")
|
|
35
|
+
return type_name
|
|
36
|
+
|
|
37
|
+
def get_component(self, type_name: str) -> type:
|
|
38
|
+
"""Retrieve a component class by its type name."""
|
|
39
|
+
with self._lock:
|
|
40
|
+
if type_name in self._components:
|
|
41
|
+
return self._components[type_name]
|
|
42
|
+
|
|
43
|
+
logger.error(f"Component class '{type_name}' not found in registry.")
|
|
44
|
+
raise KeyError(f"Component class '{type_name}' not found. Ensure it is registered.")
|
|
45
|
+
|
|
46
|
+
def get_component_type_name(self, component_class: type) -> str | None:
|
|
47
|
+
"""Get the type name for a component class, registering it if necessary."""
|
|
48
|
+
with self._lock:
|
|
49
|
+
for type_name, registered_class in self._components.items():
|
|
50
|
+
if component_class == registered_class:
|
|
51
|
+
return type_name
|
|
52
|
+
# If not found, register using class name and return
|
|
53
|
+
return self.register_component(component_class)
|
|
54
|
+
|
|
55
|
+
def get_all_components(self) -> dict[str, type]:
|
|
56
|
+
"""Get all registered components."""
|
|
57
|
+
with self._lock:
|
|
58
|
+
return self._components.copy()
|
|
59
|
+
|
|
60
|
+
def clear(self) -> None:
|
|
61
|
+
"""Clear all registered components."""
|
|
62
|
+
with self._lock:
|
|
63
|
+
self._components.clear()
|
|
64
|
+
logger.debug("Cleared all registered components")
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# src/flock/core/registry/config_mapping.py
|
|
2
|
+
"""Config-to-component mapping functionality."""
|
|
3
|
+
|
|
4
|
+
import threading
|
|
5
|
+
from typing import TypeVar
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
from flock.core.logging.logging import get_logger
|
|
9
|
+
|
|
10
|
+
logger = get_logger("registry.config_mapping")
|
|
11
|
+
|
|
12
|
+
ConfigType = TypeVar("ConfigType", bound=BaseModel)
|
|
13
|
+
ClassType = TypeVar("ClassType", bound=type)
|
|
14
|
+
|
|
15
|
+
# Global config mapping with thread safety
|
|
16
|
+
_component_config_map: dict[type[BaseModel], type] = {}
|
|
17
|
+
_config_map_lock = threading.RLock()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ConfigMapping:
|
|
21
|
+
"""Manages config-to-component mappings with thread safety."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, lock: threading.RLock):
|
|
24
|
+
self._lock = lock
|
|
25
|
+
|
|
26
|
+
def register_config_component_pair(
|
|
27
|
+
self, config_cls: type[ConfigType], component_cls: type[ClassType]
|
|
28
|
+
) -> None:
|
|
29
|
+
"""Explicitly register the mapping between a config and component class."""
|
|
30
|
+
# Component config validation can be added here if needed
|
|
31
|
+
# Add more checks if needed (e.g., component_cls inherits from Module/Router/Evaluator)
|
|
32
|
+
|
|
33
|
+
with _config_map_lock:
|
|
34
|
+
if (
|
|
35
|
+
config_cls in _component_config_map
|
|
36
|
+
and _component_config_map[config_cls] != component_cls
|
|
37
|
+
):
|
|
38
|
+
logger.warning(
|
|
39
|
+
f"Config class {config_cls.__name__} already mapped to {_component_config_map[config_cls].__name__}. "
|
|
40
|
+
f"Overwriting with {component_cls.__name__}."
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
_component_config_map[config_cls] = component_cls
|
|
44
|
+
logger.debug(
|
|
45
|
+
f"Registered config mapping: {config_cls.__name__} -> {component_cls.__name__}"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
def get_component_class_for_config(
|
|
49
|
+
self, config_cls: type[ConfigType]
|
|
50
|
+
) -> type[ClassType] | None:
|
|
51
|
+
"""Look up the Component Class associated with a Config Class."""
|
|
52
|
+
with _config_map_lock:
|
|
53
|
+
return _component_config_map.get(config_cls)
|
|
54
|
+
|
|
55
|
+
def get_all_config_mappings(self) -> dict[type[BaseModel], type]:
|
|
56
|
+
"""Get all config-to-component mappings."""
|
|
57
|
+
with _config_map_lock:
|
|
58
|
+
return _component_config_map.copy()
|
|
59
|
+
|
|
60
|
+
def clear_config_mappings(self) -> None:
|
|
61
|
+
"""Clear all config-to-component mappings."""
|
|
62
|
+
with _config_map_lock:
|
|
63
|
+
_component_config_map.clear()
|
|
64
|
+
logger.debug("Cleared all config-to-component mappings")
|