From 721a3642f34d368611f5a8c7810a4e761e86ac68 Mon Sep 17 00:00:00 2001 From: Fede Luzzi Date: Fri, 29 May 2026 17:09:27 -0300 Subject: [PATCH] bug fixes and moving to production --- README.md | 3 ++ connpy/__init__.py | 3 ++ connpy/_version.py | 2 +- connpy/ai.py | 61 ++++++++++++++------------ connpy/services/user_service.py | 2 +- docs/connpy/services/user_service.html | 4 +- setup.cfg | 2 +- 7 files changed, 45 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index b93008b..8ad45d1 100644 --- a/README.md +++ b/README.md @@ -192,3 +192,6 @@ response = myai.ask("What is the status of the BGP neighbors in the office?") --- *For detailed developer notes and plugin hooks documentation, see the [Documentation](https://fluzzi.github.io/connpy/).* + +## 📜 License +[PolyForm Noncommercial 1.0.0](LICENSE) diff --git a/connpy/__init__.py b/connpy/__init__.py index a3f0113..e7c5802 100644 --- a/connpy/__init__.py +++ b/connpy/__init__.py @@ -181,6 +181,9 @@ response = myai.ask("What is the status of the BGP neighbors in the office?") --- *For detailed developer notes and plugin hooks documentation, see the [Documentation](https://fluzzi.github.io/connpy/).* + +## 📜 License +[PolyForm Noncommercial 1.0.0](LICENSE) ''' from .core import node,nodes from .configfile import configfile diff --git a/connpy/_version.py b/connpy/_version.py index 6aadc96..0f607a5 100644 --- a/connpy/_version.py +++ b/connpy/_version.py @@ -1 +1 @@ -__version__ = "6.0.0b13" +__version__ = "6.0.0" diff --git a/connpy/ai.py b/connpy/ai.py index 3646876..a3e3175 100755 --- a/connpy/ai.py +++ b/connpy/ai.py @@ -765,13 +765,11 @@ class ai: if self.interrupted: raise KeyboardInterrupt - # Soft limit warning - if iteration == self.soft_limit_iterations and not soft_limit_warned: - self.console.print(f"[warning]⚠ Engineer has performed {iteration} steps. This is taking longer than expected.[/warning]") - self.console.print(f"[warning] You can press Ctrl+C to interrupt and get a summary.[/warning]") - soft_limit_warned = True - - if status and not chat_history: status.update(f"[ai_status]Engineer: Analyzing mission... (step {iteration})") + if status and not chat_history: + status_text = f"[ai_status]Engineer: Analyzing mission... (step {iteration})" + if iteration >= self.soft_limit_iterations: + status_text += " [warning]⚠ Taking longer than expected (Ctrl+C to interrupt)[/warning]" + status.update(status_text) try: safe_messages = self._sanitize_messages(messages) @@ -796,17 +794,23 @@ class ai: # Notificación en tiempo real de la tarea técnica (Only if not in Architect loop) if status and not chat_history: - if fn == "list_nodes": status.update(f"[ai_status]Engineer: [SEARCH] {args.get('filter_pattern','.*')}") + s_text = "" + if fn == "list_nodes": s_text = f"[ai_status]Engineer: [SEARCH] {args.get('filter_pattern','.*')}" elif fn == "run_commands": cmds = args.get('commands', []) cmd_str = cmds[0] if cmds else "" - status.update(f"[ai_status]Engineer: [CMD] {cmd_str}") - elif fn == "get_node_info": status.update(f"[ai_status]Engineer: [INSPECT] {args.get('node_name','')}") + s_text = f"[ai_status]Engineer: [CMD] {cmd_str}" + elif fn == "get_node_info": s_text = f"[ai_status]Engineer: [INSPECT] {args.get('node_name','')}" elif fn.startswith("mcp_"): server = fn.split("__")[0].replace("mcp_", "") tool = fn.split("__")[1] if "__" in fn else fn - status.update(f"[ai_status]Engineer: [MCP:{server}] {tool}") - elif fn in self.tool_status_formatters: status.update(self.tool_status_formatters[fn](args)) + s_text = f"[ai_status]Engineer: [MCP:{server}] {tool}" + elif fn in self.tool_status_formatters: s_text = self.tool_status_formatters[fn](args) + + if s_text: + if iteration >= self.soft_limit_iterations: + s_text += " [warning]⚠ Taking longer than expected (Ctrl+C to interrupt)[/warning]" + status.update(s_text) if debug: self._print_debug_observation(f"Decision: {fn}", args, status=status) @@ -1011,11 +1015,18 @@ class ai: @MethodHook def ask(self, user_input, dryrun=False, chat_history=None, status=None, debug=False, stream=True, session_id=None, chunk_callback=None): - soft_limit_warned = False is_engineer_keyless = "vertex" in self.engineer_model.lower() or "ollama" in self.engineer_model.lower() or "local" in self.engineer_model.lower() if not self.engineer_key and not self.engineer_auth and not is_engineer_keyless: raise ValueError("Engineer API key or authentication not configured. Use 'connpy config --engineer-auth ' to set it.") + def update_status(text): + if not status: + return + if iteration >= self.soft_limit_iterations: + warning_suffix = " [warning]⚠ Taking longer than expected (Ctrl+C to interrupt)[/warning]" + if warning_suffix not in text: + text += warning_suffix + status.update(text) if chat_history is None: chat_history = [] @@ -1106,18 +1117,14 @@ class ai: if self.interrupted: raise KeyboardInterrupt - # Soft limit warning - if iteration == self.soft_limit_iterations and not soft_limit_warned: - self.console.print(f"[warning]⚠ Agent has performed {iteration} steps. This is taking longer than expected.[/warning]") - self.console.print(f"[warning] You can press Ctrl+C to interrupt and get a summary of progress.[/warning]") - soft_limit_warned = True + # Soft limit warning - handled inline within update_status label = "[architect][bold]Architect[/bold][/architect]" if current_brain == "architect" else "[engineer][bold]Engineer[/bold][/engineer]" if status: # Notify responder identity for web/remote clients if getattr(status, "is_web", False) or getattr(status, "is_remote", False): status.update(f"__RESPONDER__:{current_brain}") - status.update(f"{label} is thinking... (step {iteration})") + update_status(f"{label} is thinking... (step {iteration})") streamed_response = False try: @@ -1132,7 +1139,7 @@ class ai: response = completion(model=model, messages=safe_messages, tools=tools, num_retries=3, **current_auth) except Exception as e: if current_brain == "architect": - if status: status.update("[unavailable]Architect unavailable! Falling back to Engineer...") + if status: update_status("[unavailable]Architect unavailable! Falling back to Engineer...") # Preserve context when falling back - use clean_input directly current_brain = "engineer" model = self.engineer_model @@ -1189,8 +1196,8 @@ class ai: continue if status: - if fn == "delegate_to_engineer": status.update(f"[architect]Architect: [DELEGATING MISSION] {args.get('task','')[:40]}...") - elif fn == "manage_memory_tool": status.update(f"[architect]Architect: [UPDATING MEMORY]") + if fn == "delegate_to_engineer": update_status(f"[architect]Architect: [DELEGATING MISSION] {args.get('task','')[:40]}...") + elif fn == "manage_memory_tool": update_status(f"[architect]Architect: [UPDATING MEMORY]") if debug: self._print_debug_observation(f"Decision: {fn}", args, status=status) @@ -1199,7 +1206,7 @@ class ai: obs, eng_usage = self._engineer_loop(args["task"], status=status, debug=debug, chat_history=messages[:-1]) usage["input"] += eng_usage["input"]; usage["output"] += eng_usage["output"]; usage["total"] += eng_usage["total"] elif fn == "consult_architect": - if status: status.update("[architect]Engineer consulting Architect...") + if status: update_status("[architect]Engineer consulting Architect...") try: # Consultation only - Engineer stays in control claude_resp = completion( @@ -1221,11 +1228,11 @@ class ai: try: status.start() except: pass except Exception as e: - if status: status.update("[unavailable]Architect unavailable! Engineer continuing alone...") + if status: update_status("[unavailable]Architect unavailable! Engineer continuing alone...") obs = f"Architect unavailable ({str(e)}). Proceeding with your best technical judgment." elif fn == "escalate_to_architect": - if status: status.update("[architect]Transferring control to Architect...") + if status: update_status("[architect]Transferring control to Architect...") # Full escalation - Architect takes over current_brain = "architect" model = self.architect_model @@ -1247,7 +1254,7 @@ class ai: except: pass elif fn == "return_to_engineer": - if status: status.update("[engineer]Transferring control back to Engineer...") + if status: update_status("[engineer]Transferring control back to Engineer...") # Architect returns control to Engineer current_brain = "engineer" model = self.engineer_model @@ -1300,7 +1307,7 @@ class ai: messages.append(resp_msg.model_dump(exclude_none=True)) except Exception as e: if status: - status.update(f"[error]Error fetching summary: {e}[/error]") + update_status(f"[error]Error fetching summary: {e}[/error]") printer.warning(f"Failed to fetch final summary from LLM: {e}") except KeyboardInterrupt: if status: status.update("[error]Interrupted! Closing pending tasks...") diff --git a/connpy/services/user_service.py b/connpy/services/user_service.py index b1f4c75..af40322 100644 --- a/connpy/services/user_service.py +++ b/connpy/services/user_service.py @@ -215,7 +215,7 @@ class UserService: if username not in registry["users"]: raise ValueError(f"User '{username}' not found") - expiration = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(hours=8) + expiration = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(hours=12) payload = { "sub": username, "exp": expiration diff --git a/docs/connpy/services/user_service.html b/docs/connpy/services/user_service.html index 2d6ffd5..75aaf0f 100644 --- a/docs/connpy/services/user_service.html +++ b/docs/connpy/services/user_service.html @@ -261,7 +261,7 @@ el.replaceWith(d); if username not in registry["users"]: raise ValueError(f"User '{username}' not found") - expiration = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(hours=8) + expiration = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(hours=12) payload = { "sub": username, "exp": expiration @@ -473,7 +473,7 @@ Mode B: config_path set -> Reuses existing directory after validating its str if username not in registry["users"]: raise ValueError(f"User '{username}' not found") - expiration = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(hours=8) + expiration = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(hours=12) payload = { "sub": username, "exp": expiration diff --git a/setup.cfg b/setup.cfg index ac13a05..e304fd7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -8,7 +8,7 @@ keywords = networking, automation, docker, kubernetes, ssh, telnet, connection m author = Federico Luzzi author_email = fluzzi@gmail.com url = https://github.com/fluzzi/connpy -license = Custom Software License +license = PolyForm Noncommercial License 1.0.0 license_files = LICENSE project_urls = Bug Tracker = https://github.com/fluzzi/connpy/issues