friday-framework-runtime 0.1.0a0__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.
@@ -0,0 +1,177 @@
1
+ """Runtime tool loading helpers for declarative provider-aware registry assembly."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib
6
+ import inspect
7
+ import pkgutil
8
+ from functools import lru_cache
9
+ from typing import Any
10
+
11
+ from friday_agent import DefaultToolRegistry
12
+ from friday_core.interfaces import ToolDefinition
13
+
14
+ from friday_runtime.config import ToolProviderConfig, ToolsConfig
15
+
16
+
17
+ class ToolProviderLoadError(RuntimeError):
18
+ """Raised when a configured runtime tool provider cannot be loaded."""
19
+
20
+
21
+ def register_runtime_tools(
22
+ registry: DefaultToolRegistry,
23
+ config: ToolsConfig,
24
+ ) -> None:
25
+ """Register tools from configured providers into the runtime registry."""
26
+ for tool in _select_builtin_tools(config.providers.builtin):
27
+ registry.register(tool)
28
+
29
+ for tool in _select_friday_tools(config.providers.friday_tools):
30
+ registry.register(tool)
31
+
32
+ if config.providers.mcp.enabled:
33
+ raise ToolProviderLoadError(
34
+ "tools.providers.mcp is configured, but MCP runtime loading "
35
+ "is not implemented in Phase 5-B."
36
+ )
37
+
38
+
39
+ def _select_builtin_tools(provider: ToolProviderConfig) -> list[ToolDefinition]:
40
+ if not provider.enabled:
41
+ return []
42
+
43
+ catalog = _builtin_tool_catalog()
44
+ return _filter_catalog(catalog, provider)
45
+
46
+
47
+ def _select_friday_tools(provider: ToolProviderConfig) -> list[ToolDefinition]:
48
+ if not provider.enabled:
49
+ return []
50
+
51
+ catalog = _friday_tools_catalog()
52
+ return _filter_catalog(catalog, provider)
53
+
54
+
55
+ @lru_cache(maxsize=1)
56
+ def _builtin_tool_catalog() -> tuple[ToolDefinition, ...]:
57
+ temp_registry = DefaultToolRegistry()
58
+ temp_registry.load_decorated_tools()
59
+ return tuple(sorted(temp_registry.list_tools(), key=lambda tool: tool.name))
60
+
61
+
62
+ @lru_cache(maxsize=1)
63
+ def _friday_tools_catalog() -> tuple[ToolDefinition, ...]:
64
+ try:
65
+ friday_tools = importlib.import_module("friday_tools")
66
+ except Exception as exc:
67
+ raise ToolProviderLoadError(
68
+ f"Unable to import friday_tools package for runtime discovery: {exc}"
69
+ ) from exc
70
+
71
+ modules = [friday_tools]
72
+ if hasattr(friday_tools, "__path__"):
73
+ module_names = sorted(
74
+ module_name
75
+ for _, module_name, _ in pkgutil.walk_packages(
76
+ friday_tools.__path__,
77
+ f"{friday_tools.__name__}.",
78
+ )
79
+ )
80
+ for module_name in module_names:
81
+ try:
82
+ modules.append(importlib.import_module(module_name))
83
+ except Exception as exc:
84
+ raise ToolProviderLoadError(
85
+ f"Unable to import friday_tools module '{module_name}': {exc}"
86
+ ) from exc
87
+
88
+ discovered: dict[str, ToolDefinition] = {}
89
+ for module in modules:
90
+ for value in vars(module).values():
91
+ if inspect.isclass(value) or not isinstance(value, ToolDefinition):
92
+ continue
93
+ if not isinstance(getattr(value, "name", None), str):
94
+ continue
95
+ if not isinstance(getattr(value, "category", None), str):
96
+ continue
97
+ discovered.setdefault(value.name, value)
98
+
99
+ return tuple(sorted(discovered.values(), key=lambda tool: tool.name))
100
+
101
+
102
+ def _filter_catalog(
103
+ catalog: tuple[ToolDefinition, ...],
104
+ provider: ToolProviderConfig,
105
+ ) -> list[ToolDefinition]:
106
+ by_name = {tool.name: tool for tool in catalog}
107
+
108
+ if provider.mode == "selected":
109
+ selected: dict[str, ToolDefinition] = {}
110
+ if provider.include_categories:
111
+ selected.update(
112
+ {
113
+ tool.name: tool
114
+ for tool in catalog
115
+ if tool.category in provider.include_categories
116
+ }
117
+ )
118
+ for tool_name in provider.include_tools:
119
+ tool = by_name.get(tool_name)
120
+ if tool is not None:
121
+ selected[tool_name] = tool
122
+ else:
123
+ selected = {tool.name: tool for tool in catalog}
124
+ if provider.include_categories:
125
+ selected = {
126
+ name: tool
127
+ for name, tool in selected.items()
128
+ if tool.category in provider.include_categories
129
+ }
130
+ for tool_name in provider.include_tools:
131
+ tool = by_name.get(tool_name)
132
+ if tool is not None:
133
+ selected[tool_name] = tool
134
+
135
+ if provider.exclude_categories:
136
+ selected = {
137
+ name: tool
138
+ for name, tool in selected.items()
139
+ if tool.category not in provider.exclude_categories
140
+ }
141
+
142
+ for tool_name in provider.exclude_tools:
143
+ selected.pop(tool_name, None)
144
+
145
+ return [selected[name] for name in sorted(selected)]
146
+
147
+
148
+ def validate_agent_tool_bindings(
149
+ *,
150
+ agent_name: str,
151
+ declared_tools: list[str],
152
+ available_tool_names: set[str],
153
+ ) -> None:
154
+ """Fail hard when an agent references tools unavailable in the runtime."""
155
+ missing = sorted({tool_name for tool_name in declared_tools if tool_name not in available_tool_names})
156
+ if missing:
157
+ raise ValueError(
158
+ f"Agent '{agent_name}' references unavailable tool(s): {missing}"
159
+ )
160
+
161
+
162
+ def collect_profile_tool_references(profile: Any) -> list[str]:
163
+ """Collect all tool references declared across an agent profile."""
164
+ ordered: list[str] = []
165
+ seen: set[str] = set()
166
+ for field_name in ("tools", "injected_tools", "available_tools", "required_tools"):
167
+ values = getattr(profile, field_name, [])
168
+ if not isinstance(values, list):
169
+ continue
170
+ for value in values:
171
+ if not isinstance(value, str):
172
+ continue
173
+ stripped = value.strip()
174
+ if stripped and stripped not in seen:
175
+ ordered.append(stripped)
176
+ seen.add(stripped)
177
+ return ordered