Skip to content

Commit 676072b

Browse files
authored
Merge pull request #1067 from jupyter-naas/refactor-models-loading
refactor(models): add include_models filter to selectively load models
2 parents 12e98d0 + c7ce1d8 commit 676072b

7 files changed

Lines changed: 103 additions & 9 deletions

File tree

libs/naas-abi-cli/uv.lock

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

libs/naas-abi-core/naas_abi_core/module/Module.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,9 @@ def on_load(self):
166166
# is additive.
167167
if self._engine.services.model_registry_available():
168168
ModuleModelLoader.load_models(
169-
self.__class__, self._engine.services.model_registry
169+
self.__class__,
170+
self._engine.services.model_registry,
171+
include_models=getattr(self._configuration, "include_models", None),
170172
)
171173

172174
def on_initialized(self):

libs/naas-abi-core/naas_abi_core/module/ModuleModelLoader.py

Lines changed: 79 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,22 +16,79 @@
1616
import importlib
1717
import inspect
1818
import os
19+
import re
20+
from pathlib import Path
1921
from typing import TYPE_CHECKING
2022

21-
from naas_abi_core.models.Model import Model, ModelDefinition
23+
from naas_abi_core.models.Model import CanonicalModelId, Model, ModelDefinition
2224
from naas_abi_core.module.ModuleUtils import find_class_module_root_path
2325
from naas_abi_core.utils.Logger import logger
2426

2527
if TYPE_CHECKING:
2628
from naas_abi_core.services.model_registry.ModelRegistryPort import IModelRegistry
2729

2830

31+
# Matches ``CANONICAL_ID = CanonicalModelId.FOO`` /
32+
# ``CANONICAL_ID = CanonicalModelId["FOO"]`` / ``CANONICAL_ID = "foo"`` so a
33+
# model file's canonical id can be resolved *without importing it* (importing a
34+
# model file constructs its provider client, which can be slow).
35+
_CANONICAL_ID_ASSIGN_RE = re.compile(r"CANONICAL_ID\s*=")
36+
_CANONICAL_ID_VALUE_RE = re.compile(
37+
r"CANONICAL_ID\s*=\s*(?:"
38+
r"CanonicalModelId\.([A-Za-z0-9_]+)" # enum-member access
39+
r"|CanonicalModelId\[\s*[\"']([^\"']+)[\"']\s*\]" # enum-subscript by name
40+
r"|[\"']([^\"']+)[\"']" # bare string literal
41+
r")"
42+
)
43+
44+
45+
def _static_canonical_ids(source: str) -> tuple[set[str], bool]:
46+
"""Best-effort extraction of the canonical-id strings declared via
47+
``CANONICAL_ID = ...`` in a model source file, without importing it.
48+
49+
Returns ``(canonical_ids, fully_resolved)``. ``fully_resolved`` is ``False``
50+
when at least one ``CANONICAL_ID`` assignment could not be resolved to a
51+
concrete string — callers must not skip a file that isn't fully resolved.
52+
"""
53+
ids: set[str] = set()
54+
resolved = 0
55+
for member, subscript, literal in _CANONICAL_ID_VALUE_RE.findall(source):
56+
if member:
57+
try:
58+
ids.add(CanonicalModelId[member].value)
59+
resolved += 1
60+
except KeyError:
61+
# Unknown member name — leave unresolved so we import to be safe.
62+
pass
63+
elif subscript:
64+
ids.add(subscript)
65+
resolved += 1
66+
elif literal:
67+
ids.add(literal)
68+
resolved += 1
69+
total = len(_CANONICAL_ID_ASSIGN_RE.findall(source))
70+
return ids, resolved == total
71+
72+
2973
class ModuleModelLoader:
3074
@classmethod
31-
def load_models(cls, class_: type, registry: "IModelRegistry") -> int:
75+
def load_models(
76+
cls,
77+
class_: type,
78+
registry: "IModelRegistry",
79+
include_models: list[str] | None = None,
80+
) -> int:
3281
"""Recursively walk ``<module_root>/models/`` and register every
3382
``ModelDefinition`` subclass found, including those in provider
3483
subdirectories (e.g. ``models/openai/``, ``models/anthropic/``).
84+
85+
When ``include_models`` is provided, only models whose ``CANONICAL_ID``
86+
is in that list are registered; every other model file is skipped
87+
*without importing it* — importing a model file constructs its provider
88+
client (e.g. a Bedrock client), which can add seconds per file. Files
89+
whose canonical id cannot be resolved statically are imported anyway, so
90+
the filter never drops a model it can't positively identify.
91+
3592
Returns the count of successful registrations."""
3693
module_root_path = find_class_module_root_path(class_)
3794
models_path = module_root_path / "models"
@@ -40,6 +97,7 @@ def load_models(cls, class_: type, registry: "IModelRegistry") -> int:
4097
return 0
4198

4299
logger.debug(f"Loading models from {models_path}")
100+
include_set = set(include_models) if include_models is not None else None
43101
top_package = class_.__module__.split(".")[0]
44102
registered = 0
45103

@@ -64,6 +122,25 @@ def load_models(cls, class_: type, registry: "IModelRegistry") -> int:
64122
f"{class_.__module__}.models.{subpackage}.{file[:-3]}"
65123
)
66124

125+
# Honour include_models without paying the import cost: if the
126+
# file's canonical id(s) can be resolved statically and none are
127+
# in the include set, skip before importing.
128+
if include_set is not None:
129+
try:
130+
source = (Path(dirpath) / file).read_text(encoding="utf-8")
131+
except OSError:
132+
source = None
133+
if source is not None:
134+
ids, fully_resolved = _static_canonical_ids(source)
135+
if fully_resolved and ids and ids.isdisjoint(include_set):
136+
logger.debug(
137+
"ModuleModelLoader: skipping %s — canonical id(s) "
138+
"%s not in include_models.",
139+
model_module_path,
140+
sorted(ids),
141+
)
142+
continue
143+
67144
try:
68145
model_module = importlib.import_module(model_module_path)
69146
except Exception as exc:

libs/naas-abi-marketplace/naas_abi_marketplace/ai/bedrock/__init__.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ class Configuration(ModuleConfiguration):
4747
region_name: "us-east-1"
4848
validate_on_load: true
4949
validation_model_id: null # optional: invoke this model to prove access
50+
include_models: # optional: register only these canonical ids
51+
- "gpt-oss-120b"
5052
"""
5153

5254
aws_access_key_id: Optional[str] = None
@@ -55,6 +57,13 @@ class Configuration(ModuleConfiguration):
5557
region_name: str = "us-east-1"
5658
datastore_path: str = "bedrock"
5759

60+
# When set, only models whose CANONICAL_ID is in this list are
61+
# registered on load; every other models/*.py file is skipped *without
62+
# importing it*, which avoids constructing the (slow) Bedrock client for
63+
# models you don't use. Values are canonical model ids, e.g.
64+
# ["gpt-oss-120b"]. When None (default), all discovered models load.
65+
include_models: Optional[list[str]] = None
66+
5867
# When true (default), the module verifies on load that:
5968
# 1. boto3 can resolve AWS credentials,
6069
# 2. sts:GetCallerIdentity succeeds,

libs/naas-abi-marketplace/naas_abi_marketplace/applications/openrouter/agents/OpenRouterAgent.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
from typing import Optional
44

5+
from langchain_core.language_models import BaseChatModel
6+
7+
from naas_abi_core.models.Model import ChatModel
58
from naas_abi_core.services.agent.IntentAgent import (
69
AgentConfiguration,
710
AgentSharedState,
@@ -86,6 +89,7 @@ def New(
8689
if cls.MODEL_ID and "/" in cls.MODEL_ID
8790
else cls.MODEL_ID
8891
)
92+
chat_model: BaseChatModel | ChatModel
8993
if lookup_id in registry.list_canonical_ids():
9094
chat_model = registry.get_chat_model(lookup_id, provider="openrouter")
9195
else:

libs/naas-abi/naas_abi/apps/nexus/apps/api/app/services/agents/adapters/primary/agents__primary_adapter__FastAPI.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,8 @@ def _public_modules_url(path: str) -> str:
218218
from naas_abi import ABIModule
219219

220220
public_api_host = ABIModule.get_instance().configuration.global_config.public_api_host
221+
if not public_api_host.startswith("https://"):
222+
public_api_host = f"https://{public_api_host}"
221223
return f"{public_api_host}/modules/{path.lstrip('/')}"
222224

223225

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)