perf(ai): decouple AI suite from startup and enable lazy loading across services and CLI

- Decouple connapp.ai from get_parser() via DeferredAIProxy to queue plugin modifications without importing connpy.ai or Pydantic v2 upfront.
- Guard connapp.start() finally block to invoke ai.cleanup() only if connpy.ai was actively imported.
- Defer CopilotInterface and AIService imports in core.py to execute strictly upon receiving Copilot hotkey (\x00).
- Convert heavy services (ai, sync, users, import_export, system, execution) into lazy properties on ServiceProvider and services/__init__.py.
- Implement O(1) globals() dict caching in module __getattr__ (PEP 562) across connpy/__init__.py and services/__init__.py.
- Defer Google Drive SDK in sync_service.py and interactive CLI components (inquirer) across handlers.
- Reduced CLI startup & parser initialization latency from ~1.47s to ~183ms (~87% improvement).
This commit is contained in:
2026-08-11 19:31:55 -03:00
parent bf5bddda5a
commit 835283eba6
8 changed files with 72 additions and 20 deletions
+8 -2
View File
@@ -282,12 +282,18 @@ from .core import node,nodes
from .configfile import configfile from .configfile import configfile
from .connapp import connapp from .connapp import connapp
from .api import * from .api import *
from .ai import ai
from .plugins import Plugins from .plugins import Plugins
from ._version import __version__ from ._version import __version__
from . import printer from . import printer
__all__ = ["node", "nodes", "configfile", "connapp", "ai", "Plugins", "printer"] def __getattr__(name: str):
if name == "ai":
from .ai import ai
globals()["ai"] = ai
return ai
raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
__all__ = ["node", "nodes", "configfile", "connapp", "Plugins", "printer"]
__author__ = "Federico Luzzi" __author__ = "Federico Luzzi"
__pdoc__ = { __pdoc__ = {
'core': False, 'core': False,
+1 -1
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import sys import sys
from connpy import * from connpy import configfile, connapp
def main(): def main():
conf = configfile() conf = configfile()
+2 -1
View File
@@ -21,14 +21,15 @@ from prompt_toolkit.history import InMemoryHistory
from ..printer import connpy_theme from ..printer import connpy_theme
from connpy.utils import log_cleaner from connpy.utils import log_cleaner
from ..services.ai_service import AIService
class CopilotInterface: class CopilotInterface:
def __init__(self, config, history=None, pt_input=None, pt_output=None, rich_file=None, session_state=None): def __init__(self, config, history=None, pt_input=None, pt_output=None, rich_file=None, session_state=None):
from ..services.ai_service import AIService
self.config = config self.config = config
self.history = history or InMemoryHistory() self.history = history or InMemoryHistory()
self.pt_input = pt_input self.pt_input = pt_input
self.pt_output = pt_output self.pt_output = pt_output
self.rich_file = rich_file
self.ai_service = AIService(config) self.ai_service = AIService(config)
self.mode_range, self.mode_single, self.mode_lines = 0, 1, 2 self.mode_range, self.mode_single, self.mode_lines = 0, 1, 2
+35 -9
View File
@@ -10,7 +10,6 @@ from .core import node,nodes
from ._version import __version__ from ._version import __version__
from . import printer from . import printer
from .api import start_api,stop_api,debug_api from .api import start_api,stop_api,debug_api
from .ai import ai
from .plugins import Plugins from .plugins import Plugins
from .services.exceptions import ConnpyError, ProfileNotFoundError, ReservedNameError from .services.exceptions import ConnpyError, ProfileNotFoundError, ReservedNameError
@@ -41,6 +40,32 @@ console = printer.console
#functions and classes #functions and classes
class DeferredAIProxy:
"""Proxy for connapp.ai that defers importing connpy.ai until ai is actually invoked or accessed."""
def __init__(self):
self._deferred_modifications = []
self._real_ai = None
def _load_real_ai(self):
if self._real_ai is None:
from .ai import ai
self._real_ai = ai
for mod in self._deferred_modifications:
self._real_ai.modify(mod)
return self._real_ai
def modify(self, modification_func):
if self._real_ai is not None:
self._real_ai.modify(modification_func)
else:
self._deferred_modifications.append(modification_func)
def __call__(self, *args, **kwargs):
return self._load_real_ai()(*args, **kwargs)
def __getattr__(self, name):
return getattr(self._load_real_ai(), name)
class connapp: class connapp:
''' This class starts the connection manager app. It's normally used by connection manager but you can use it on a script to run the connection manager your way and use a different configfile and key. ''' This class starts the connection manager app. It's normally used by connection manager but you can use it on a script to run the connection manager your way and use a different configfile and key.
''' '''
@@ -72,8 +97,8 @@ class connapp:
self.start_api = start_api self.start_api = start_api
self.stop_api = stop_api # Using SystemService logic eventually self.stop_api = stop_api # Using SystemService logic eventually
self.debug_api = debug_api self.debug_api = debug_api
self.ai = ai self.ai = DeferredAIProxy()
# Register context filtering hooks (only on Client CLI, bypass on gRPC Server) # Register context filtering hooks (only on Client CLI, bypass on gRPC Server)
is_api_server = len(sys.argv) > 1 and sys.argv[1] == "api" is_api_server = len(sys.argv) > 1 and sys.argv[1] == "api"
if not is_api_server: if not is_api_server:
@@ -524,12 +549,13 @@ class connapp:
printer.warning("Operation cancelled by user.") printer.warning("Operation cancelled by user.")
sys.exit(130) sys.exit(130)
finally: finally:
# Safely cleanup AI sessions (litellm) # Safely cleanup AI sessions (litellm) if AI was loaded
try: if "connpy.ai" in sys.modules:
from .ai import cleanup try:
cleanup() from .ai import cleanup
except ImportError: cleanup()
pass except (ImportError, Exception):
pass
class _store_type(argparse.Action): class _store_type(argparse.Action):
#Custom store type for cli app. #Custom store type for cli app.
+3 -2
View File
@@ -712,13 +712,14 @@ class node:
def _copilot_handler(self, config): def _copilot_handler(self, config):
"""Unified copilot handler for local session.""" """Unified copilot handler for local session."""
from .cli.terminal_ui import CopilotInterface
from .services.ai_service import AIService
import asyncio import asyncio
import os import os
async def handler(buffer, node_info, stream, child_fd, cmd_byte_positions=None): async def handler(buffer, node_info, stream, child_fd, cmd_byte_positions=None):
try: try:
from .cli.terminal_ui import CopilotInterface
from .services.ai_service import AIService
interface = CopilotInterface( interface = CopilotInterface(
config, config,
history=getattr(stream, 'copilot_history', None), history=getattr(stream, 'copilot_history', None),
+9 -1
View File
@@ -1,26 +1,34 @@
from .exceptions import * from .exceptions import *
from .node_service import NodeService from .node_service import NodeService
from .profile_service import ProfileService from .profile_service import ProfileService
from .ai_service import AIService
from .plugin_service import PluginService from .plugin_service import PluginService
from .config_service import ConfigService from .config_service import ConfigService
def __getattr__(name: str): def __getattr__(name: str):
if name == "ExecutionService": if name == "ExecutionService":
from .execution_service import ExecutionService from .execution_service import ExecutionService
globals()["ExecutionService"] = ExecutionService
return ExecutionService return ExecutionService
elif name == "ImportExportService": elif name == "ImportExportService":
from .import_export_service import ImportExportService from .import_export_service import ImportExportService
globals()["ImportExportService"] = ImportExportService
return ImportExportService return ImportExportService
elif name == "SystemService": elif name == "SystemService":
from .system_service import SystemService from .system_service import SystemService
globals()["SystemService"] = SystemService
return SystemService return SystemService
elif name == "SyncService": elif name == "SyncService":
from .sync_service import SyncService from .sync_service import SyncService
globals()["SyncService"] = SyncService
return SyncService return SyncService
elif name == "UserService": elif name == "UserService":
from .user_service import UserService from .user_service import UserService
globals()["UserService"] = UserService
return UserService return UserService
elif name == "AIService":
from .ai_service import AIService
globals()["AIService"] = AIService
return AIService
raise AttributeError(f"module '{__name__}' has no attribute '{name}'") raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
__all__ = [ __all__ = [
+2 -2
View File
@@ -271,13 +271,13 @@ class PluginService(BaseService):
is_mock = True is_mock = True
def __init__(self, config): def __init__(self, config):
from ..core import node, nodes from ..core import node, nodes
from ..ai import ai from ..connapp import DeferredAIProxy
from ..services.provider import ServiceProvider from ..services.provider import ServiceProvider
self.config = config self.config = config
self.node = node self.node = node
self.nodes = nodes self.nodes = nodes
self.ai = ai self.ai = DeferredAIProxy()
self.services = ServiceProvider(config, mode="local") self.services = ServiceProvider(config, mode="local")
+12 -2
View File
@@ -19,6 +19,7 @@ class ServiceProvider:
self._import_export = None self._import_export = None
self._sync = None self._sync = None
self._users = None self._users = None
self._ai = None
if mode == "local": if mode == "local":
self._init_local() self._init_local()
@@ -32,14 +33,12 @@ class ServiceProvider:
from .profile_service import ProfileService from .profile_service import ProfileService
from .config_service import ConfigService from .config_service import ConfigService
from .plugin_service import PluginService from .plugin_service import PluginService
from .ai_service import AIService
from .context_service import ContextService from .context_service import ContextService
self.nodes = NodeService(self.config) self.nodes = NodeService(self.config)
self.profiles = ProfileService(self.config) self.profiles = ProfileService(self.config)
self.config_svc = ConfigService(self.config) self.config_svc = ConfigService(self.config)
self.plugins = PluginService(self.config) self.plugins = PluginService(self.config)
self.ai = AIService(self.config)
self.context = ContextService(self.config) self.context = ContextService(self.config)
def _init_remote(self): def _init_remote(self):
@@ -145,3 +144,14 @@ class ServiceProvider:
@users.setter @users.setter
def users(self, value): def users(self, value):
self._users = value self._users = value
@property
def ai(self):
if self._ai is None and self.mode == "local":
from .ai_service import AIService
self._ai = AIService(self.config)
return self._ai
@ai.setter
def ai(self, value):
self._ai = value