bug fixes and moving to production

This commit is contained in:
2026-05-29 17:09:27 -03:00
parent e52d300cf1
commit 721a3642f3
7 changed files with 45 additions and 32 deletions
+3
View File
@@ -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/).* *For detailed developer notes and plugin hooks documentation, see the [Documentation](https://fluzzi.github.io/connpy/).*
## 📜 License
[PolyForm Noncommercial 1.0.0](LICENSE)
+3
View File
@@ -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/).* *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 .core import node,nodes
from .configfile import configfile from .configfile import configfile
+1 -1
View File
@@ -1 +1 @@
__version__ = "6.0.0b13" __version__ = "6.0.0"
+34 -27
View File
@@ -765,13 +765,11 @@ class ai:
if self.interrupted: if self.interrupted:
raise KeyboardInterrupt raise KeyboardInterrupt
# Soft limit warning if status and not chat_history:
if iteration == self.soft_limit_iterations and not soft_limit_warned: status_text = f"[ai_status]Engineer: Analyzing mission... (step {iteration})"
self.console.print(f"[warning]⚠ Engineer has performed {iteration} steps. This is taking longer than expected.[/warning]") if iteration >= self.soft_limit_iterations:
self.console.print(f"[warning] You can press Ctrl+C to interrupt and get a summary.[/warning]") status_text += " [warning]⚠ Taking longer than expected (Ctrl+C to interrupt)[/warning]"
soft_limit_warned = True status.update(status_text)
if status and not chat_history: status.update(f"[ai_status]Engineer: Analyzing mission... (step {iteration})")
try: try:
safe_messages = self._sanitize_messages(messages) 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) # Notificación en tiempo real de la tarea técnica (Only if not in Architect loop)
if status and not chat_history: 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": elif fn == "run_commands":
cmds = args.get('commands', []) cmds = args.get('commands', [])
cmd_str = cmds[0] if cmds else "" cmd_str = cmds[0] if cmds else ""
status.update(f"[ai_status]Engineer: [CMD] {cmd_str}") s_text = f"[ai_status]Engineer: [CMD] {cmd_str}"
elif fn == "get_node_info": status.update(f"[ai_status]Engineer: [INSPECT] {args.get('node_name','')}") elif fn == "get_node_info": s_text = f"[ai_status]Engineer: [INSPECT] {args.get('node_name','')}"
elif fn.startswith("mcp_"): elif fn.startswith("mcp_"):
server = fn.split("__")[0].replace("mcp_", "") server = fn.split("__")[0].replace("mcp_", "")
tool = fn.split("__")[1] if "__" in fn else fn tool = fn.split("__")[1] if "__" in fn else fn
status.update(f"[ai_status]Engineer: [MCP:{server}] {tool}") s_text = f"[ai_status]Engineer: [MCP:{server}] {tool}"
elif fn in self.tool_status_formatters: status.update(self.tool_status_formatters[fn](args)) 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: if debug:
self._print_debug_observation(f"Decision: {fn}", args, status=status) self._print_debug_observation(f"Decision: {fn}", args, status=status)
@@ -1011,11 +1015,18 @@ class ai:
@MethodHook @MethodHook
def ask(self, user_input, dryrun=False, chat_history=None, status=None, debug=False, stream=True, session_id=None, chunk_callback=None): 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() 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: 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 <auth>' to set it.") raise ValueError("Engineer API key or authentication not configured. Use 'connpy config --engineer-auth <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 = [] if chat_history is None: chat_history = []
@@ -1106,18 +1117,14 @@ class ai:
if self.interrupted: if self.interrupted:
raise KeyboardInterrupt raise KeyboardInterrupt
# Soft limit warning # Soft limit warning - handled inline within update_status
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
label = "[architect][bold]Architect[/bold][/architect]" if current_brain == "architect" else "[engineer][bold]Engineer[/bold][/engineer]" label = "[architect][bold]Architect[/bold][/architect]" if current_brain == "architect" else "[engineer][bold]Engineer[/bold][/engineer]"
if status: if status:
# Notify responder identity for web/remote clients # Notify responder identity for web/remote clients
if getattr(status, "is_web", False) or getattr(status, "is_remote", False): if getattr(status, "is_web", False) or getattr(status, "is_remote", False):
status.update(f"__RESPONDER__:{current_brain}") 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 streamed_response = False
try: try:
@@ -1132,7 +1139,7 @@ class ai:
response = completion(model=model, messages=safe_messages, tools=tools, num_retries=3, **current_auth) response = completion(model=model, messages=safe_messages, tools=tools, num_retries=3, **current_auth)
except Exception as e: except Exception as e:
if current_brain == "architect": 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 # Preserve context when falling back - use clean_input directly
current_brain = "engineer" current_brain = "engineer"
model = self.engineer_model model = self.engineer_model
@@ -1189,8 +1196,8 @@ class ai:
continue continue
if status: if status:
if fn == "delegate_to_engineer": status.update(f"[architect]Architect: [DELEGATING MISSION] {args.get('task','')[:40]}...") if fn == "delegate_to_engineer": update_status(f"[architect]Architect: [DELEGATING MISSION] {args.get('task','')[:40]}...")
elif fn == "manage_memory_tool": status.update(f"[architect]Architect: [UPDATING MEMORY]") elif fn == "manage_memory_tool": update_status(f"[architect]Architect: [UPDATING MEMORY]")
if debug: if debug:
self._print_debug_observation(f"Decision: {fn}", args, status=status) 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]) 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"] usage["input"] += eng_usage["input"]; usage["output"] += eng_usage["output"]; usage["total"] += eng_usage["total"]
elif fn == "consult_architect": elif fn == "consult_architect":
if status: status.update("[architect]Engineer consulting Architect...") if status: update_status("[architect]Engineer consulting Architect...")
try: try:
# Consultation only - Engineer stays in control # Consultation only - Engineer stays in control
claude_resp = completion( claude_resp = completion(
@@ -1221,11 +1228,11 @@ class ai:
try: status.start() try: status.start()
except: pass except: pass
except Exception as e: 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." obs = f"Architect unavailable ({str(e)}). Proceeding with your best technical judgment."
elif fn == "escalate_to_architect": 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 # Full escalation - Architect takes over
current_brain = "architect" current_brain = "architect"
model = self.architect_model model = self.architect_model
@@ -1247,7 +1254,7 @@ class ai:
except: pass except: pass
elif fn == "return_to_engineer": 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 # Architect returns control to Engineer
current_brain = "engineer" current_brain = "engineer"
model = self.engineer_model model = self.engineer_model
@@ -1300,7 +1307,7 @@ class ai:
messages.append(resp_msg.model_dump(exclude_none=True)) messages.append(resp_msg.model_dump(exclude_none=True))
except Exception as e: except Exception as e:
if status: 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}") printer.warning(f"Failed to fetch final summary from LLM: {e}")
except KeyboardInterrupt: except KeyboardInterrupt:
if status: status.update("[error]Interrupted! Closing pending tasks...") if status: status.update("[error]Interrupted! Closing pending tasks...")
+1 -1
View File
@@ -215,7 +215,7 @@ class UserService:
if username not in registry["users"]: if username not in registry["users"]:
raise ValueError(f"User '{username}' not found") 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 = { payload = {
"sub": username, "sub": username,
"exp": expiration "exp": expiration
+2 -2
View File
@@ -261,7 +261,7 @@ el.replaceWith(d);
if username not in registry[&#34;users&#34;]: if username not in registry[&#34;users&#34;]:
raise ValueError(f&#34;User &#39;{username}&#39; not found&#34;) raise ValueError(f&#34;User &#39;{username}&#39; not found&#34;)
expiration = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(hours=8) expiration = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(hours=12)
payload = { payload = {
&#34;sub&#34;: username, &#34;sub&#34;: username,
&#34;exp&#34;: expiration &#34;exp&#34;: expiration
@@ -473,7 +473,7 @@ Mode B: config_path set -&gt; Reuses existing directory after validating its str
if username not in registry[&#34;users&#34;]: if username not in registry[&#34;users&#34;]:
raise ValueError(f&#34;User &#39;{username}&#39; not found&#34;) raise ValueError(f&#34;User &#39;{username}&#39; not found&#34;)
expiration = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(hours=8) expiration = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(hours=12)
payload = { payload = {
&#34;sub&#34;: username, &#34;sub&#34;: username,
&#34;exp&#34;: expiration &#34;exp&#34;: expiration
+1 -1
View File
@@ -8,7 +8,7 @@ keywords = networking, automation, docker, kubernetes, ssh, telnet, connection m
author = Federico Luzzi author = Federico Luzzi
author_email = fluzzi@gmail.com author_email = fluzzi@gmail.com
url = https://github.com/fluzzi/connpy url = https://github.com/fluzzi/connpy
license = Custom Software License license = PolyForm Noncommercial License 1.0.0
license_files = LICENSE license_files = LICENSE
project_urls = project_urls =
Bug Tracker = https://github.com/fluzzi/connpy/issues Bug Tracker = https://github.com/fluzzi/connpy/issues