diff --git a/connpy/__init__.py b/connpy/__init__.py index 0570077..15532f4 100644 --- a/connpy/__init__.py +++ b/connpy/__init__.py @@ -282,12 +282,18 @@ from .core import node,nodes from .configfile import configfile from .connapp import connapp from .api import * -from .ai import ai from .plugins import Plugins from ._version import __version__ 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" __pdoc__ = { 'core': False, diff --git a/connpy/__main__.py b/connpy/__main__.py index 5924061..267caf1 100644 --- a/connpy/__main__.py +++ b/connpy/__main__.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 import sys -from connpy import * +from connpy import configfile, connapp def main(): conf = configfile() diff --git a/connpy/cli/terminal_ui.py b/connpy/cli/terminal_ui.py index ca63763..0515f00 100644 --- a/connpy/cli/terminal_ui.py +++ b/connpy/cli/terminal_ui.py @@ -21,14 +21,15 @@ from prompt_toolkit.history import InMemoryHistory from ..printer import connpy_theme from connpy.utils import log_cleaner -from ..services.ai_service import AIService class CopilotInterface: 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.history = history or InMemoryHistory() self.pt_input = pt_input self.pt_output = pt_output + self.rich_file = rich_file self.ai_service = AIService(config) self.mode_range, self.mode_single, self.mode_lines = 0, 1, 2 diff --git a/connpy/connapp.py b/connpy/connapp.py index 21450dc..a582f04 100755 --- a/connpy/connapp.py +++ b/connpy/connapp.py @@ -10,7 +10,6 @@ from .core import node,nodes from ._version import __version__ from . import printer from .api import start_api,stop_api,debug_api -from .ai import ai from .plugins import Plugins from .services.exceptions import ConnpyError, ProfileNotFoundError, ReservedNameError @@ -41,6 +40,32 @@ console = printer.console #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: ''' 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.stop_api = stop_api # Using SystemService logic eventually self.debug_api = debug_api - self.ai = ai - + self.ai = DeferredAIProxy() + # Register context filtering hooks (only on Client CLI, bypass on gRPC Server) is_api_server = len(sys.argv) > 1 and sys.argv[1] == "api" if not is_api_server: @@ -524,12 +549,13 @@ class connapp: printer.warning("Operation cancelled by user.") sys.exit(130) finally: - # Safely cleanup AI sessions (litellm) - try: - from .ai import cleanup - cleanup() - except ImportError: - pass + # Safely cleanup AI sessions (litellm) if AI was loaded + if "connpy.ai" in sys.modules: + try: + from .ai import cleanup + cleanup() + except (ImportError, Exception): + pass class _store_type(argparse.Action): #Custom store type for cli app. diff --git a/connpy/core.py b/connpy/core.py index 2f6ca09..0024643 100755 --- a/connpy/core.py +++ b/connpy/core.py @@ -712,13 +712,14 @@ class node: def _copilot_handler(self, config): """Unified copilot handler for local session.""" - from .cli.terminal_ui import CopilotInterface - from .services.ai_service import AIService import asyncio import os async def handler(buffer, node_info, stream, child_fd, cmd_byte_positions=None): try: + from .cli.terminal_ui import CopilotInterface + from .services.ai_service import AIService + interface = CopilotInterface( config, history=getattr(stream, 'copilot_history', None), diff --git a/connpy/services/__init__.py b/connpy/services/__init__.py index e90507f..7968af2 100644 --- a/connpy/services/__init__.py +++ b/connpy/services/__init__.py @@ -1,26 +1,34 @@ from .exceptions import * from .node_service import NodeService from .profile_service import ProfileService -from .ai_service import AIService from .plugin_service import PluginService from .config_service import ConfigService def __getattr__(name: str): if name == "ExecutionService": from .execution_service import ExecutionService + globals()["ExecutionService"] = ExecutionService return ExecutionService elif name == "ImportExportService": from .import_export_service import ImportExportService + globals()["ImportExportService"] = ImportExportService return ImportExportService elif name == "SystemService": from .system_service import SystemService + globals()["SystemService"] = SystemService return SystemService elif name == "SyncService": from .sync_service import SyncService + globals()["SyncService"] = SyncService return SyncService elif name == "UserService": from .user_service import UserService + globals()["UserService"] = 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}'") __all__ = [ diff --git a/connpy/services/plugin_service.py b/connpy/services/plugin_service.py index 938adde..2c43c25 100644 --- a/connpy/services/plugin_service.py +++ b/connpy/services/plugin_service.py @@ -271,13 +271,13 @@ class PluginService(BaseService): is_mock = True def __init__(self, config): from ..core import node, nodes - from ..ai import ai + from ..connapp import DeferredAIProxy from ..services.provider import ServiceProvider self.config = config self.node = node self.nodes = nodes - self.ai = ai + self.ai = DeferredAIProxy() self.services = ServiceProvider(config, mode="local") diff --git a/connpy/services/provider.py b/connpy/services/provider.py index 0fafdc3..a403d4d 100644 --- a/connpy/services/provider.py +++ b/connpy/services/provider.py @@ -19,6 +19,7 @@ class ServiceProvider: self._import_export = None self._sync = None self._users = None + self._ai = None if mode == "local": self._init_local() @@ -32,14 +33,12 @@ class ServiceProvider: from .profile_service import ProfileService from .config_service import ConfigService from .plugin_service import PluginService - from .ai_service import AIService from .context_service import ContextService self.nodes = NodeService(self.config) self.profiles = ProfileService(self.config) self.config_svc = ConfigService(self.config) self.plugins = PluginService(self.config) - self.ai = AIService(self.config) self.context = ContextService(self.config) def _init_remote(self): @@ -145,3 +144,14 @@ class ServiceProvider: @users.setter def users(self, 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