Compare commits

...

8 Commits

Author SHA1 Message Date
fluzzi32 6ee953edcf RC version 2026-05-27 12:34:52 -03:00
fluzzi32 74756f25b2 fix paignation block detection 2026-05-22 13:22:54 -03:00
fluzzi32 243718df46 improve blocks in range 2026-05-22 11:01:36 -03:00
fluzzi32 3be9935541 support fro multiple litellm auth methods 2026-05-21 18:20:24 -03:00
fluzzi32 cd8eeaad79 fix blocks in web 2026-05-21 13:03:12 -03:00
fluzzi32 4f3af7ca12 fix bugs long commands in cisco 2026-05-20 17:23:57 -03:00
fluzzi32 dce9982454 feat(ai): enhance session management, inquirer theme, and gRPC MCP support
- AI & Session Management:
  - Add random suffix to session IDs to ensure uniqueness.
  - Implement optional pagination/limit in session listing (default 20).
  - Add `--all` flag to `ai` CLI commands to view all sessions.
  - Keep active session ID and path synced correctly during clean session startups.

- CLI UI/UX:
  - Add a custom `ConnpyTheme` for inquirer prompts that dynamically translates
    active hex style colors to terminal ANSI/blessed escapes.

- gRPC & Services:
  - Implement remote MCP server listing (`list_mcp_servers` RPC and services).
  - Stream responder updates (`__RESPONDER__`) to toggle headers dynamically between
    "Network Engineer" and "Network Architect" on remote/web clients.
  - Fix remote client deadlock risk by ensuring `final_mark` is sent on exceptions.
  - Hydrate client-side chat history correctly on initial streaming request.

- Testing:
  - Add integration tests for AI gRPC services and MCP server listing.
2026-05-20 12:27:02 -03:00
fluzzi32 468868ac18 bug fix remote lenght 2026-05-19 16:23:35 -03:00
69 changed files with 2866 additions and 1795 deletions
+5
View File
@@ -146,11 +146,13 @@ package.json
# Development docs # Development docs
connpy_roadmap.md connpy_roadmap.md
testnew/
testall/ testall/
testremote/ testremote/
*.db *.db
*.patch *.patch
scratch.py scratch.py
connpy.code-workspace
# Internal planning and implementation docs # Internal planning and implementation docs
PLAN_CAPA_SERVICIOS.md PLAN_CAPA_SERVICIOS.md
@@ -170,3 +172,6 @@ MULTI_USER_IMPLEMENTATION_STEPS.md
#themes #themes
nord.yml nord.yml
theme.py theme.py
#ai auth
auth.json
+4
View File
@@ -17,8 +17,12 @@ The v6 release introduces the **AI Copilot**, an interactive terminal assistant
## 🤖 AI Copilot (New in v6) ## 🤖 AI Copilot (New in v6)
The AI Copilot is deeply integrated into your terminal workflow: The AI Copilot is deeply integrated into your terminal workflow:
- **Terminal Context Awareness**: The Copilot can "see" your screen output, helping you diagnose errors or analyze command results in real-time. - **Terminal Context Awareness**: The Copilot can "see" your screen output, helping you diagnose errors or analyze command results in real-time.
- **Dynamic Context Selection**: Flexibly select single, range, or line-based terminal blocks to feed the Copilot, filtering out interactive scrolling garbage automatically (e.g., Cisco IOS/XR scrolling, paginators).
- **Hybrid Multi-Agent System**: Automatically escalates complex tasks between the **Network Engineer** (execution) and the **Network Architect** (strategy). - **Hybrid Multi-Agent System**: Automatically escalates complex tasks between the **Network Engineer** (execution) and the **Network Architect** (strategy).
- **MCP Integration**: Dynamically load tools from external providers (6WIND, AWS, etc.) via the Model Context Protocol. - **MCP Integration**: Dynamically load tools from external providers (6WIND, AWS, etc.) via the Model Context Protocol.
- **Flexible Auth & Keyless AI**: Support for advanced LiteLLM credentials (`--engineer-auth` / `--architect-auth`) allowing keyless local models (Ollama), cloud engines (Vertex AI), or custom endpoints.
- **Enhanced Session Management**: Uniquely generated sessions, robust pagination, and interactive styling translating prompt themes directly to terminal escapes.
- **Semantic Prompt Integration**: Emit standard OSC prompt sequences (`\x1b]133;B`) for real-time remote/web front-end command tracking.
- **Interactive Chat**: Launch with `conn ai` for a collaborative troubleshooting session. - **Interactive Chat**: Launch with `conn ai` for a collaborative troubleshooting session.
+7 -1
View File
@@ -19,8 +19,12 @@ The v6 release introduces the **AI Copilot**, an interactive terminal assistant
## 🤖 AI Copilot (New in v6) ## 🤖 AI Copilot (New in v6)
The AI Copilot is deeply integrated into your terminal workflow: The AI Copilot is deeply integrated into your terminal workflow:
- **Terminal Context Awareness**: The Copilot can "see" your screen output, helping you diagnose errors or analyze command results in real-time. - **Terminal Context Awareness**: The Copilot can "see" your screen output, helping you diagnose errors or analyze command results in real-time.
- **Dynamic Context Selection**: Flexibly select single, range, or line-based terminal blocks to feed the Copilot, filtering out interactive scrolling garbage automatically (e.g., Cisco IOS/XR scrolling, paginators).
- **Hybrid Multi-Agent System**: Automatically escalates complex tasks between the **Network Engineer** (execution) and the **Network Architect** (strategy). - **Hybrid Multi-Agent System**: Automatically escalates complex tasks between the **Network Engineer** (execution) and the **Network Architect** (strategy).
- **MCP Integration**: Dynamically load tools from external providers (6WIND, AWS, etc.) via the Model Context Protocol. - **MCP Integration**: Dynamically load tools from external providers (6WIND, AWS, etc.) via the Model Context Protocol.
- **Flexible Auth & Keyless AI**: Support for advanced LiteLLM credentials (`--engineer-auth` / `--architect-auth`) allowing keyless local models (Ollama), cloud engines (Vertex AI), or custom endpoints.
- **Enhanced Session Management**: Uniquely generated sessions, robust pagination, and interactive styling translating prompt themes directly to terminal escapes.
- **Semantic Prompt Integration**: Emit standard OSC prompt sequences (`\x1b]133;B`) for real-time remote/web front-end command tracking.
- **Interactive Chat**: Launch with `conn ai` for a collaborative troubleshooting session. - **Interactive Chat**: Launch with `conn ai` for a collaborative troubleshooting session.
@@ -203,5 +207,7 @@ __pdoc__ = {
'nodes.deferred_class_hooks': False, 'nodes.deferred_class_hooks': False,
'connapp': False, 'connapp': False,
'connapp.encrypt': True, 'connapp.encrypt': True,
'printer': False 'printer': False,
'tests': False
} }
+1 -1
View File
@@ -1 +1 @@
__version__ = "6.0.0b11" __version__ = "6.0.0b12"
+83 -28
View File
@@ -1,4 +1,6 @@
import os import os
import secrets
import sys import sys
import json import json
import re import re
@@ -106,7 +108,7 @@ class ai:
r'^systemctl\s+status\s+', r'^journalctl\s+' r'^systemctl\s+status\s+', r'^journalctl\s+'
] ]
def __init__(self, config, org=None, api_key=None, engineer_model=None, architect_model=None, engineer_api_key=None, architect_api_key=None, console=None, confirm_handler=None, trust=False): def __init__(self, config, org=None, api_key=None, engineer_model=None, architect_model=None, engineer_api_key=None, architect_api_key=None, console=None, confirm_handler=None, trust=False, engineer_auth=None, architect_auth=None, **kwargs):
self.config = config self.config = config
self.console = console or printer.console self.console = console or printer.console
self.confirm_handler = confirm_handler or self._local_confirm_handler self.confirm_handler = confirm_handler or self._local_confirm_handler
@@ -125,6 +127,29 @@ class ai:
self.engineer_key = engineer_api_key or aiconfig.get("engineer_api_key") self.engineer_key = engineer_api_key or aiconfig.get("engineer_api_key")
self.architect_key = architect_api_key or aiconfig.get("architect_api_key") self.architect_key = architect_api_key or aiconfig.get("architect_api_key")
# Auth configurations (Prioridad: Argumento -> Config)
self.engineer_auth = engineer_auth if engineer_auth is not None else aiconfig.get("engineer_auth")
if self.engineer_auth is None:
self.engineer_auth = {}
elif not isinstance(self.engineer_auth, dict):
self.engineer_auth = {}
self.architect_auth = architect_auth if architect_auth is not None else aiconfig.get("architect_auth")
if self.architect_auth is None:
self.architect_auth = {}
elif not isinstance(self.architect_auth, dict):
self.architect_auth = {}
# Backward compatibility fallbacks: only inject api_key if the auth dict is empty/not configured
if self.engineer_key and not self.engineer_auth:
self.engineer_auth["api_key"] = self.engineer_key
if self.architect_key and not self.architect_auth:
self.architect_auth["api_key"] = self.architect_key
# Strategic Reasoning Engine (Architect) availability
is_architect_keyless = "vertex" in self.architect_model.lower() or "ollama" in self.architect_model.lower() or "local" in self.architect_model.lower()
self.has_architect = bool(self.architect_key or self.architect_auth or is_architect_keyless)
# Custom Trusted Commands Regexes # Custom Trusted Commands Regexes
custom_trusted = aiconfig.get("trusted_commands", []) custom_trusted = aiconfig.get("trusted_commands", [])
if isinstance(custom_trusted, str): if isinstance(custom_trusted, str):
@@ -165,12 +190,12 @@ class ai:
# Session Management # Session Management
self.sessions_dir = os.path.join(self.config.defaultdir, "ai_sessions") self.sessions_dir = os.path.join(self.config.defaultdir, "ai_sessions")
os.makedirs(self.sessions_dir, exist_ok=True) os.makedirs(self.sessions_dir, exist_ok=True)
self.session_id = None self.session_id = getattr(self.config, "session_id", None)
self.session_path = None self.session_path = os.path.join(self.sessions_dir, f"{self.session_id}.json") if self.session_id else None
# Prompts base agnósticos # Prompts base agnósticos
architect_instructions = "" architect_instructions = ""
if self.architect_key: if self.has_architect:
architect_instructions = """ architect_instructions = """
CRITICAL - CONSULT vs ESCALATE: CRITICAL - CONSULT vs ESCALATE:
- ALWAYS use 'consult_architect' for: Configuration planning, design decisions, complex troubleshooting. - ALWAYS use 'consult_architect' for: Configuration planning, design decisions, complex troubleshooting.
@@ -186,7 +211,7 @@ class ai:
else: else:
architect_instructions = """ architect_instructions = """
CRITICAL - ARCHITECT UNAVAILABLE: CRITICAL - ARCHITECT UNAVAILABLE:
- The Strategic Reasoning Engine (Architect) is currently UNAVAILABLE because its API key is not configured. - The Strategic Reasoning Engine (Architect) is currently UNAVAILABLE because its API key or authentication is not configured.
- DO NOT attempt to consult or escalate to the architect. - DO NOT attempt to consult or escalate to the architect.
- If the user asks to consult the architect, inform them that the Architect is offline and offer to help them directly to the best of your abilities. - If the user asks to consult the architect, inform them that the Architect is offline and offer to help them directly to the best of your abilities.
""" """
@@ -292,15 +317,19 @@ class ai:
if status_formatter: if status_formatter:
self.tool_status_formatters[name] = status_formatter self.tool_status_formatters[name] = status_formatter
def _stream_completion(self, model, messages, tools, api_key, status=None, label="", debug=False, chunk_callback=None, **kwargs): def _stream_completion(self, model, messages, tools, api_key=None, status=None, label="", debug=False, chunk_callback=None, auth=None, **kwargs):
"""Stream a completion call, rendering styled Markdown in real-time. """Stream a completion call, rendering styled Markdown in real-time.
Returns (response, streamed) where: Returns (response, streamed) where:
- response: reconstructed ModelResponse (same as non-streaming) - response: reconstructed ModelResponse (same as non-streaming)
- streamed: True if text was rendered to console during streaming - streamed: True if text was rendered to console during streaming
""" """
auth_dict = auth if auth is not None else {}
if api_key and "api_key" not in auth_dict:
auth_dict = auth_dict.copy()
auth_dict["api_key"] = api_key
stream_resp = completion(model=model, messages=messages, tools=tools, api_key=api_key, stream=True, **kwargs) stream_resp = completion(model=model, messages=messages, tools=tools, stream=True, **auth_dict, **kwargs)
chunks = [] chunks = []
full_content = "" full_content = ""
@@ -743,7 +772,7 @@ class ai:
try: try:
safe_messages = self._sanitize_messages(messages) safe_messages = self._sanitize_messages(messages)
response = completion(model=self.engineer_model, messages=safe_messages, tools=tools, api_key=self.engineer_key) response = completion(model=self.engineer_model, messages=safe_messages, tools=tools, **self.engineer_auth)
except Exception as e: except Exception as e:
if status: status.stop() if status: status.stop()
raise ValueError(f"Engineer failed to connect: {str(e)}") raise ValueError(f"Engineer failed to connect: {str(e)}")
@@ -877,16 +906,27 @@ class ai:
continue continue
return sorted(sessions, key=lambda x: x["created_at"], reverse=True) return sorted(sessions, key=lambda x: x["created_at"], reverse=True)
def list_sessions(self): def list_sessions(self, limit=20):
"""Prints a list of sessions using printer.table.""" """Prints a list of sessions using printer.table."""
sessions = self._get_sessions() sessions = self._get_sessions()
if not sessions: if not sessions:
printer.info("No saved AI sessions found.") printer.info("No saved AI sessions found.")
return return
total = len(sessions)
if limit and total > limit:
sessions = sessions[:limit]
columns = ["ID", "Title", "Created At", "Model"] columns = ["ID", "Title", "Created At", "Model"]
rows = [[s["id"], s["title"], s["created_at"], s["model"]] for s in sessions] rows = [[s["id"], s["title"], s["created_at"], s["model"]] for s in sessions]
printer.table("AI Persisted Sessions", columns, rows)
title = "AI Persisted Sessions"
if limit and total > limit:
title += f" (Showing last {limit} of {total})"
printer.table(title, columns, rows)
if limit and total > limit:
printer.info(f"Use '--list --all' (if supported) or check the sessions directory to see all {total} sessions.")
def load_session_data(self, session_id): def load_session_data(self, session_id):
"""Loads a session's raw data by ID.""" """Loads a session's raw data by ID."""
@@ -917,8 +957,10 @@ class ai:
return sessions[0]["id"] if sessions else None return sessions[0]["id"] if sessions else None
def _generate_session_id(self, query): def _generate_session_id(self, query):
"""Generates a unique session ID based on timestamp.""" """Generates a unique session ID based on timestamp and a random suffix."""
return datetime.datetime.now().strftime("%Y%m%d-%H%M%S") ts = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
suffix = secrets.token_hex(2)
return f"{ts}-{suffix}"
def save_session(self, history, title=None, model=None): def save_session(self, history, title=None, model=None):
"""Saves current history to the session file.""" """Saves current history to the session file."""
@@ -927,6 +969,8 @@ class ai:
first_user_msg = next((m["content"] for m in history if m["role"] == "user"), "new-session") first_user_msg = next((m["content"] for m in history if m["role"] == "user"), "new-session")
self.session_id = self._generate_session_id(first_user_msg) self.session_id = self._generate_session_id(first_user_msg)
self.session_path = os.path.join(self.sessions_dir, f"{self.session_id}.json") self.session_path = os.path.join(self.sessions_dir, f"{self.session_id}.json")
elif not self.session_path:
self.session_path = os.path.join(self.sessions_dir, f"{self.session_id}.json")
# If it's a new file, we might want to set a better title # If it's a new file, we might want to set a better title
if not os.path.exists(self.session_path) and not title: if not os.path.exists(self.session_path) and not title:
@@ -964,16 +1008,22 @@ 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):
if not self.engineer_key: is_engineer_keyless = "vertex" in self.engineer_model.lower() or "ollama" in self.engineer_model.lower() or "local" in self.engineer_model.lower()
raise ValueError("Engineer API key not configured. Use 'connpy config --engineer-api-key <key>' to set it.") 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.")
if chat_history is None: chat_history = [] if chat_history is None: chat_history = []
# Load session if provided and history is empty # Load session if provided and history is empty
if session_id and not chat_history: if session_id:
session_data = self.load_session_data(session_id) # Force the session_id even if it doesn't exist yet
if session_data: self.session_id = session_id
chat_history = session_data.get("history", []) self.session_path = os.path.join(self.sessions_dir, f"{session_id}.json")
if not chat_history:
session_data = self.load_session_data(session_id)
if session_data:
chat_history = session_data.get("history", [])
# If we loaded history, the caller might need it back # If we loaded history, the caller might need it back
# But typically ask() is called in a loop with an external history object # But typically ask() is called in a loop with an external history object
@@ -1009,6 +1059,7 @@ class ai:
tools = self._get_architect_tools() if current_brain == "architect" else self._get_engineer_tools() tools = self._get_architect_tools() if current_brain == "architect" else self._get_engineer_tools()
model = self.architect_model if current_brain == "architect" else self.engineer_model model = self.architect_model if current_brain == "architect" else self.engineer_model
key = self.architect_key if current_brain == "architect" else self.engineer_key key = self.architect_key if current_brain == "architect" else self.engineer_key
current_auth = self.architect_auth if current_brain == "architect" else self.engineer_auth
# Estructura optimizada para Prompt Caching (Solo para Anthropic directo, Vertex tiene reglas distintas) # Estructura optimizada para Prompt Caching (Solo para Anthropic directo, Vertex tiene reglas distintas)
if "claude" in model.lower() and "vertex" not in model.lower(): if "claude" in model.lower() and "vertex" not in model.lower():
@@ -1058,8 +1109,8 @@ class ai:
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 ONLY for web/remote clients (StatusBridge has is_web) # Notify responder identity for web/remote clients
if getattr(status, "is_web", 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})") status.update(f"{label} is thinking... (step {iteration})")
@@ -1068,12 +1119,12 @@ class ai:
safe_messages = self._sanitize_messages(messages) safe_messages = self._sanitize_messages(messages)
if stream: if stream:
response, streamed_response = self._stream_completion( response, streamed_response = self._stream_completion(
model=model, messages=safe_messages, tools=tools, api_key=key, model=model, messages=safe_messages, tools=tools, auth=current_auth,
status=status, label=label, debug=debug, num_retries=3, status=status, label=label, debug=debug, num_retries=3,
chunk_callback=chunk_callback chunk_callback=chunk_callback
) )
else: else:
response = completion(model=model, messages=safe_messages, tools=tools, api_key=key, num_retries=3) 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: status.update("[unavailable]Architect unavailable! Falling back to Engineer...")
@@ -1082,6 +1133,7 @@ class ai:
model = self.engineer_model model = self.engineer_model
tools = self._get_engineer_tools() tools = self._get_engineer_tools()
key = self.engineer_key key = self.engineer_key
current_auth = self.engineer_auth
# Rebuild messages with Engineer system prompt and original user request # Rebuild messages with Engineer system prompt and original user request
messages = [{"role": "system", "content": self.engineer_system_prompt}] messages = [{"role": "system", "content": self.engineer_system_prompt}]
# Add chat history if exists (excluding system prompt) # Add chat history if exists (excluding system prompt)
@@ -1174,6 +1226,7 @@ class ai:
model = self.architect_model model = self.architect_model
tools = self._get_architect_tools() tools = self._get_architect_tools()
key = self.architect_key key = self.architect_key
current_auth = self.architect_auth
messages[0] = {"role": "system", "content": self.architect_system_prompt} messages[0] = {"role": "system", "content": self.architect_system_prompt}
# Prepare handover context to inject AFTER all tool responses # Prepare handover context to inject AFTER all tool responses
handover_msg = f"HANDOVER FROM EXECUTION ENGINE\n\nReason: {args['reason']}\n\nContext: {args['context']}\n\nYou are now in control of this conversation." handover_msg = f"HANDOVER FROM EXECUTION ENGINE\n\nReason: {args['reason']}\n\nContext: {args['context']}\n\nYou are now in control of this conversation."
@@ -1195,6 +1248,7 @@ class ai:
model = self.engineer_model model = self.engineer_model
tools = self._get_engineer_tools() tools = self._get_engineer_tools()
key = self.engineer_key key = self.engineer_key
current_auth = self.engineer_auth
messages[0] = {"role": "system", "content": self.engineer_system_prompt} messages[0] = {"role": "system", "content": self.engineer_system_prompt}
# Prepare handover context to inject AFTER all tool responses # Prepare handover context to inject AFTER all tool responses
handover_msg = f"HANDOVER FROM ARCHITECT\n\nSummary: {args['summary']}\n\nYou are now back in control. Continue handling the user's requests." handover_msg = f"HANDOVER FROM ARCHITECT\n\nSummary: {args['summary']}\n\nYou are now back in control. Continue handling the user's requests."
@@ -1236,7 +1290,7 @@ class ai:
messages.append({"role": "user", "content": "Hard iteration limit reached. Please provide a summary of your findings so far."}) messages.append({"role": "user", "content": "Hard iteration limit reached. Please provide a summary of your findings so far."})
try: try:
safe_messages = self._sanitize_messages(messages) safe_messages = self._sanitize_messages(messages)
response = completion(model=model, messages=safe_messages, tools=[], api_key=key) response = completion(model=model, messages=safe_messages, tools=[], **current_auth)
resp_msg = response.choices[0].message resp_msg = response.choices[0].message
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:
@@ -1256,7 +1310,7 @@ class ai:
try: try:
safe_messages = self._sanitize_messages(summary_messages) safe_messages = self._sanitize_messages(summary_messages)
# Use tools=None to force a text summary during interruption # Use tools=None to force a text summary during interruption
response = completion(model=model, messages=safe_messages, tools=None, api_key=key) response = completion(model=model, messages=safe_messages, tools=None, **current_auth)
resp_msg = response.choices[0].message resp_msg = response.choices[0].message
messages.append(resp_msg.model_dump(exclude_none=True)) messages.append(resp_msg.model_dump(exclude_none=True))
@@ -1393,6 +1447,7 @@ Node: {node_name}"""
# Use models based on persona # Use models based on persona
current_model = self.architect_model if persona == "architect" else self.engineer_model current_model = self.architect_model if persona == "architect" else self.engineer_model
current_key = self.architect_key if persona == "architect" else self.engineer_key current_key = self.architect_key if persona == "architect" else self.engineer_key
current_auth = self.architect_auth if persona == "architect" else self.engineer_auth
try: try:
while iteration < max_iterations: while iteration < max_iterations:
@@ -1402,8 +1457,8 @@ Node: {node_name}"""
model=current_model, model=current_model,
messages=messages, messages=messages,
tools=mcp_tools if mcp_tools else None, tools=mcp_tools if mcp_tools else None,
api_key=current_key, stream=True,
stream=True **current_auth
) )
full_content = "" full_content = ""
@@ -1476,8 +1531,8 @@ Node: {node_name}"""
model=self.engineer_model, model=self.engineer_model,
messages=messages, messages=messages,
tools=None, tools=None,
api_key=self.engineer_key, stream=True,
stream=True **self.engineer_auth
) )
full_content = "" full_content = ""
+59 -15
View File
@@ -15,13 +15,22 @@ class AIHandler:
def dispatch(self, args): def dispatch(self, args):
if args.list_sessions: if args.list_sessions:
sessions = self.app.services.ai.list_sessions() limit = 20 if not getattr(args, "all", False) else None
sessions, total = self.app.services.ai.list_sessions(limit=limit)
if not sessions: if not sessions:
printer.info("No saved AI sessions found.") printer.info("No saved AI sessions found.")
return return
columns = ["ID", "Title", "Created At", "Model"] columns = ["ID", "Title", "Created At", "Model"]
rows = [[s["id"], s["title"], s["created_at"], s["model"]] for s in sessions] rows = [[s["id"], s["title"], s["created_at"], s["model"]] for s in sessions]
printer.table("AI Persisted Sessions", columns, rows)
title = "AI Persisted Sessions"
if limit and total > limit:
title += f" (Showing last {limit} of {total})"
printer.table(title, columns, rows)
if limit and total > limit:
printer.info(f"Use '--list --all' to see all {total} sessions.")
return return
if args.delete_session: if args.delete_session:
@@ -38,7 +47,7 @@ class AIHandler:
# Determinar session_id para retomar # Determinar session_id para retomar
session_id = None session_id = None
if args.resume: if args.resume:
sessions = self.app.services.ai.list_sessions() sessions, _ = self.app.services.ai.list_sessions()
session_id = sessions[0]["id"] if sessions else None session_id = sessions[0]["id"] if sessions else None
if not session_id: if not session_id:
printer.warning("No previous session found to resume.") printer.warning("No previous session found to resume.")
@@ -57,15 +66,22 @@ class AIHandler:
elif settings.get(key): elif settings.get(key):
arguments[key] = settings.get(key) arguments[key] = settings.get(key)
for key in ["engineer_auth", "architect_auth"]:
cli_val = getattr(args, key, None)
if cli_val:
arguments[key] = self._parse_auth_value(cli_val[0])
elif settings.get(key):
arguments[key] = settings.get(key)
# Check keys only if running in local mode (not remote) # Check keys only if running in local mode (not remote)
if getattr(self.app.services, "mode", "local") == "local": if getattr(self.app.services, "mode", "local") == "local":
if not arguments.get("engineer_api_key"): if not arguments.get("engineer_api_key") and not arguments.get("engineer_auth"):
printer.error("Engineer API key not configured. The chat cannot start.") printer.error("Engineer API key/auth not configured. The chat cannot start.")
printer.info("Use 'connpy config --engineer-api-key <key>' to set it.") printer.info("Use 'connpy config --engineer-api-key <key>' or 'connpy config --engineer-auth <auth>' to set it.")
sys.exit(1) sys.exit(1)
if not arguments.get("architect_api_key"): if not arguments.get("architect_api_key") and not arguments.get("architect_auth"):
printer.warning("Architect API key not configured. Architect will be unavailable.") printer.warning("Architect API key/auth not configured. Architect will be unavailable.")
printer.info("Use 'connpy config --architect-api-key <key>' to enable it.") printer.info("Use 'connpy config --architect-api-key <key>' or 'connpy config --architect-auth <auth>' to enable it.")
# El resto de la interacción el CLI la maneja con el agente subyacente # El resto de la interacción el CLI la maneja con el agente subyacente
self.app.myai = self.app.services.ai self.app.myai = self.app.services.ai
@@ -102,7 +118,7 @@ class AIHandler:
if history: if history:
mdprint(f"[debug]Analyzing {len(history)} previous messages...[/debug]\n") mdprint(f"[debug]Analyzing {len(history)} previous messages...[/debug]\n")
else: else:
printer.error(f"Could not load session {session_id}. Starting clean.") printer.info(f"Session '{session_id}' not found. Starting clean.")
if not history: if not history:
mdprint(Rule(style="engineer")) mdprint(Rule(style="engineer"))
@@ -116,7 +132,7 @@ class AIHandler:
if user_query.lower() in ['exit', 'quit', 'bye', 'cancel']: break if user_query.lower() in ['exit', 'quit', 'bye', 'cancel']: break
with console.status("[ai_status]Agent is thinking...") as status: with console.status("[ai_status]Agent is thinking...") as status:
result = self.app.myai.ask(user_query, chat_history=history, status=status, debug=args.debug, trust=args.trust, **self.ai_overrides) result = self.app.myai.ask(user_query, chat_history=history, status=status, debug=args.debug, trust=args.trust, session_id=session_id, **self.ai_overrides)
new_history = result.get("chat_history") new_history = result.get("chat_history")
if new_history is not None: if new_history is not None:
@@ -147,8 +163,7 @@ class AIHandler:
action = mcp_args[0].lower() action = mcp_args[0].lower()
if action == "list": if action == "list":
settings = self.app.services.config_svc.get_settings() mcp_servers = self.app.services.ai.list_mcp_servers()
mcp_servers = settings.get("ai", {}).get("mcp_servers", {})
if not mcp_servers: if not mcp_servers:
printer.info("No MCP servers configured.") printer.info("No MCP servers configured.")
else: else:
@@ -213,8 +228,7 @@ class AIHandler:
from .forms import Forms from .forms import Forms
self.app.cli_forms = Forms(self.app) self.app.cli_forms = Forms(self.app)
settings = self.app.services.config_svc.get_settings() mcp_servers = self.app.services.ai.list_mcp_servers()
mcp_servers = settings.get("ai", {}).get("mcp_servers", {})
result = self.app.cli_forms.mcp_wizard(mcp_servers) result = self.app.cli_forms.mcp_wizard(mcp_servers)
if not result: if not result:
@@ -249,3 +263,33 @@ class AIHandler:
except Exception as e: except Exception as e:
printer.error(str(e)) printer.error(str(e))
def _parse_auth_value(self, value):
if not value or value.lower() in ["none", "clear"]:
return None
import os
import yaml
import json
if os.path.exists(value):
try:
with open(value, "r") as f:
content = f.read()
try:
return json.loads(content)
except ValueError:
return yaml.safe_load(content)
except Exception as e:
printer.error(f"Failed to read/parse auth file '{value}': {e}")
sys.exit(1)
try:
return json.loads(value)
except ValueError:
try:
parsed = yaml.safe_load(value)
if isinstance(parsed, dict):
return parsed
raise ValueError()
except Exception:
printer.error("Auth parameter must be a valid JSON/YAML string, or a path to a JSON/YAML file.")
sys.exit(1)
+52 -2
View File
@@ -19,8 +19,10 @@ class ConfigHandler:
"theme": self.set_theme, "theme": self.set_theme,
"engineer_model": self.set_ai_config, "engineer_model": self.set_ai_config,
"engineer_api_key": self.set_ai_config, "engineer_api_key": self.set_ai_config,
"engineer_auth": self.set_ai_config,
"architect_model": self.set_ai_config, "architect_model": self.set_ai_config,
"architect_api_key": self.set_ai_config, "architect_api_key": self.set_ai_config,
"architect_auth": self.set_ai_config,
"trusted_commands": self.set_ai_config, "trusted_commands": self.set_ai_config,
"service_mode": self.set_service_mode, "service_mode": self.set_service_mode,
"remote_host": self.set_remote_host, "remote_host": self.set_remote_host,
@@ -127,9 +129,57 @@ class ConfigHandler:
try: try:
settings = self.app.services.config_svc.get_settings() settings = self.app.services.config_svc.get_settings()
aiconfig = settings.get("ai", {}) aiconfig = settings.get("ai", {})
aiconfig[args.command] = args.data[0] val = args.data[0]
# Check for unset/clear request
if val.lower() in ["none", "clear", ""]:
if args.command in aiconfig:
del aiconfig[args.command]
else:
# If configuring auth, parse as dictionary (JSON/YAML or file path)
if args.command in ["engineer_auth", "architect_auth"]:
parsed_val = self._parse_auth_value(val)
if parsed_val is not None:
aiconfig[args.command] = parsed_val
else:
if args.command in aiconfig:
del aiconfig[args.command]
else:
aiconfig[args.command] = val
self.app.services.config_svc.update_setting("ai", aiconfig) self.app.services.config_svc.update_setting("ai", aiconfig)
printer.success("Config saved") printer.success("Config saved")
except ConnpyError as e: except (ConnpyError, InvalidConfigurationError) as e:
printer.error(str(e)) printer.error(str(e))
def _parse_auth_value(self, value):
if value.lower() in ["none", "clear", ""]:
return None
# Check if it's a file path
import os
if os.path.exists(value):
try:
with open(value, "r") as f:
content = f.read()
import json
try:
return json.loads(content)
except ValueError:
return yaml.safe_load(content)
except Exception as e:
raise InvalidConfigurationError(f"Failed to read/parse auth file '{value}': {e}")
# Try parsing as inline JSON/YAML
try:
import json
return json.loads(value)
except ValueError:
try:
parsed = yaml.safe_load(value)
if isinstance(parsed, dict):
return parsed
raise ValueError()
except Exception:
raise InvalidConfigurationError("Auth parameter must be a valid JSON/YAML string, or a path to a JSON/YAML file.")
+72 -1
View File
@@ -1,10 +1,81 @@
import os import os
import inquirer import inquirer
from inquirer.themes import Default, term
try: try:
from pyfzf.pyfzf import FzfPrompt from pyfzf.pyfzf import FzfPrompt
except ImportError: except ImportError:
FzfPrompt = None FzfPrompt = None
def hex_to_blessed(hex_str):
"""Convert hex color string to blessed/ansi format."""
if not hex_str or not isinstance(hex_str, str):
return term.normal
# Check for bold prefix
prefix = ""
if hex_str.startswith('bold '):
prefix = term.bold
hex_str = hex_str.replace('bold ', '').strip()
# If it's a standard color name
if not hex_str.startswith('#'):
return prefix + getattr(term, hex_str, term.normal)
# Parse hex
try:
h = hex_str.lstrip('#')
if len(h) == 3:
h = ''.join([c*2 for c in h])
r = int(h[0:2], 16)
g = int(h[2:4], 16)
b = int(h[4:6], 16)
# Try RGB, fallback to standard cyan if it fails or returns empty
try:
c = term.color_rgb(r, g, b)
if not c: # Some terms return empty for RGB
return prefix + term.cyan
return prefix + c
except:
return prefix + term.cyan
except:
return prefix + term.normal
# Custom inquirer theme matching connpy colors
class ConnpyTheme(Default):
def __init__(self):
super().__init__()
try:
from ..printer import _global_active_styles
# Use user_prompt as primary accent, fallback to info/cyan
accent = _global_active_styles.get("user_prompt", _global_active_styles.get("info", "cyan"))
accent_color = hex_to_blessed(accent)
self.Question.mark_color = accent_color
self.List.selection_color = accent_color
self.List.selection_cursor = ">"
except:
# Absolute fallback to standard cyan
self.Question.mark_color = term.cyan
self.List.selection_color = term.bold_cyan
self.List.selection_cursor = ">"
def get_theme():
"""Returns a fresh instance of the theme with current colors."""
return ConnpyTheme()
class ThemeProxy:
"""Proxy to ensure theme colors are resolved at runtime."""
def __getattr__(self, name):
return getattr(get_theme(), name)
def __iter__(self):
return iter(get_theme())
def __getitem__(self, item):
return get_theme()[item]
theme = ThemeProxy()
def get_config_dir(): def get_config_dir():
home = os.path.expanduser("~") home = os.path.expanduser("~")
defaultdir = os.path.join(home, '.config/conn') defaultdir = os.path.join(home, '.config/conn')
@@ -56,7 +127,7 @@ def choose(app, list_, name, action):
return answer[0] return answer[0]
else: else:
questions = [inquirer.List(name, message="Pick {} to {}:".format(name,action), choices=list_, carousel=True)] questions = [inquirer.List(name, message="Pick {} to {}:".format(name,action), choices=list_, carousel=True)]
answer = inquirer.prompt(questions) answer = inquirer.prompt(questions, theme=theme)
if answer == None: if answer == None:
return None return None
else: else:
+3 -1
View File
@@ -134,7 +134,8 @@ class CopilotInterface:
if state['context_mode'] == self.mode_single: if state['context_mode'] == self.mode_single:
active_raw = raw_bytes[start:end] active_raw = raw_bytes[start:end]
else: else:
active_raw = raw_bytes[start:] # Concat only the bytes of valid blocks to skip intermediate empty/cancelled prompt noise
active_raw = b"".join(raw_bytes[b[0]:b[1]] for b in blocks[idx:])
return preview + "\n" + log_cleaner(active_raw.decode(errors='replace')) return preview + "\n" + log_cleaner(active_raw.decode(errors='replace'))
def get_prompt_text(): def get_prompt_text():
@@ -335,6 +336,7 @@ class CopilotInterface:
persona_title = "Network Architect" if active_persona == "architect" else "Network Engineer" persona_title = "Network Architect" if active_persona == "architect" else "Network Engineer"
active_buffer = get_active_buffer() active_buffer = get_active_buffer()
live_text = "" live_text = ""
first_chunk = True first_chunk = True
+18 -16
View File
@@ -181,11 +181,28 @@ def _build_tree(nodes, folders, profiles, plugins, configdir):
ai_dict = {"__exclude_used__": True, "--help": None, "-h": None} ai_dict = {"__exclude_used__": True, "--help": None, "-h": None}
for opt in ["--engineer-model", "--engineer-api-key", "--architect-model", "--architect-api-key"]: for opt in ["--engineer-model", "--engineer-api-key", "--architect-model", "--architect-api-key"]:
ai_dict[opt] = {"*": ai_dict} # takes value, loops back ai_dict[opt] = {"*": ai_dict} # takes value, loops back
ai_dict["--engineer-auth"] = {"__extra__": lambda w: get_cwd(w, "--engineer-auth"), "*": ai_dict}
ai_dict["--architect-auth"] = {"__extra__": lambda w: get_cwd(w, "--architect-auth"), "*": ai_dict}
for opt in ["--debug", "--trust", "--list", "--list-sessions", "--session", "--resume", "--delete", "--delete-session", "-y"]: for opt in ["--debug", "--trust", "--list", "--list-sessions", "--session", "--resume", "--delete", "--delete-session", "-y"]:
ai_dict[opt] = ai_dict # takes no value, loops back ai_dict[opt] = ai_dict # takes no value, loops back
ai_dict["--mcp"] = mcp_dict ai_dict["--mcp"] = mcp_dict
ai_dict["*"] = ai_dict ai_dict["*"] = ai_dict
config_dict = {
"--allow-uppercase": ["true", "false"],
"--fzf": ["true", "false"],
"--completion": ["bash", "zsh"],
"--fzf-wrapper": ["bash", "zsh"],
"--service-mode": ["local", "remote"],
"--sync-remote": ["true", "false"],
"--help": None, "-h": None,
}
for opt in ["--keepalive", "--engineer-model", "--engineer-api-key", "--architect-model", "--architect-api-key", "--theme", "--remote", "--trusted-commands"]:
config_dict[opt] = {"*": config_dict}
config_dict["--configfolder"] = {"__extra__": lambda w: get_cwd(w, "--configfolder", True), "*": config_dict}
config_dict["--engineer-auth"] = {"__extra__": lambda w: get_cwd(w, "--engineer-auth"), "*": config_dict}
config_dict["--architect-auth"] = {"__extra__": lambda w: get_cwd(w, "--architect-auth"), "*": config_dict}
mv_state = {"__extra__": _nodes, "--help": None, "-h": None} mv_state = {"__extra__": _nodes, "--help": None, "-h": None}
cp_state = {"__extra__": _nodes, "--help": None, "-h": None} cp_state = {"__extra__": _nodes, "--help": None, "-h": None}
ls_state = { ls_state = {
@@ -280,22 +297,7 @@ def _build_tree(nodes, folders, profiles, plugins, configdir):
"--list": None, "--help": None, "--list": None, "--help": None,
"-h": None, "-h": None,
}, },
"config": { "config": config_dict,
"--allow-uppercase": ["true", "false"],
"--fzf": ["true", "false"],
"--keepalive": None,
"--completion": ["bash", "zsh"],
"--fzf-wrapper": ["bash", "zsh"],
"--configfolder": lambda w: get_cwd(w, "--configfolder", True),
"--engineer-model": None, "--engineer-api-key": None,
"--architect-model": None, "--architect-api-key": None,
"--theme": None,
"--service-mode": ["local", "remote"],
"--remote": None,
"--sync-remote": ["true", "false"],
"--trusted-commands": None,
"--help": None, "-h": None,
},
"sync": { "sync": {
"--login": None, "--logout": None, "--login": None, "--logout": None,
"--status": None, "--list": None, "--status": None, "--list": None,
+5
View File
@@ -276,11 +276,14 @@ class connapp:
aiparser.add_argument("ask", nargs='*', help="Ask connpy AI something") aiparser.add_argument("ask", nargs='*', help="Ask connpy AI something")
aiparser.add_argument("--engineer-model", nargs=1, help="Override engineer model") aiparser.add_argument("--engineer-model", nargs=1, help="Override engineer model")
aiparser.add_argument("--engineer-api-key", nargs=1, help="Override engineer api key") aiparser.add_argument("--engineer-api-key", nargs=1, help="Override engineer api key")
aiparser.add_argument("--engineer-auth", nargs=1, help="Override engineer auth (inline JSON/YAML or file path)")
aiparser.add_argument("--architect-model", nargs=1, help="Override architect model") aiparser.add_argument("--architect-model", nargs=1, help="Override architect model")
aiparser.add_argument("--architect-api-key", nargs=1, help="Override architect api key") aiparser.add_argument("--architect-api-key", nargs=1, help="Override architect api key")
aiparser.add_argument("--architect-auth", nargs=1, help="Override architect auth (inline JSON/YAML or file path)")
aiparser.add_argument("--debug", action="store_true", help="Show AI reasoning and tool calls") aiparser.add_argument("--debug", action="store_true", help="Show AI reasoning and tool calls")
aiparser.add_argument("-y", "--trust", action="store_true", help="Trust AI to execute unsafe commands without confirmation") aiparser.add_argument("-y", "--trust", action="store_true", help="Trust AI to execute unsafe commands without confirmation")
aiparser.add_argument("--list", "--list-sessions", dest="list_sessions", action="store_true", help="List saved AI sessions") aiparser.add_argument("--list", "--list-sessions", dest="list_sessions", action="store_true", help="List saved AI sessions")
aiparser.add_argument("--all", action="store_true", help="Show all sessions without limit")
aiparser.add_argument("--session", nargs=1, help="Resume a specific AI session by ID") aiparser.add_argument("--session", nargs=1, help="Resume a specific AI session by ID")
aiparser.add_argument("--resume", action="store_true", help="Resume the most recent AI session") aiparser.add_argument("--resume", action="store_true", help="Resume the most recent AI session")
aiparser.add_argument("--delete", "--delete-session", dest="delete_session", nargs=1, help="Delete an AI session by ID") aiparser.add_argument("--delete", "--delete-session", dest="delete_session", nargs=1, help="Delete an AI session by ID")
@@ -340,11 +343,13 @@ class connapp:
configcrud.add_argument("--configfolder", dest="configfolder", nargs=1, action=self._store_type, help="Set the default location for config file", metavar="FOLDER") configcrud.add_argument("--configfolder", dest="configfolder", nargs=1, action=self._store_type, help="Set the default location for config file", metavar="FOLDER")
configcrud.add_argument("--engineer-model", dest="engineer_model", nargs=1, action=self._store_type, help="Set engineer model", metavar="MODEL") configcrud.add_argument("--engineer-model", dest="engineer_model", nargs=1, action=self._store_type, help="Set engineer model", metavar="MODEL")
configcrud.add_argument("--engineer-api-key", dest="engineer_api_key", nargs=1, action=self._store_type, help="Set engineer api_key", metavar="API_KEY") configcrud.add_argument("--engineer-api-key", dest="engineer_api_key", nargs=1, action=self._store_type, help="Set engineer api_key", metavar="API_KEY")
configcrud.add_argument("--engineer-auth", dest="engineer_auth", nargs=1, action=self._store_type, help="Set engineer auth (inline JSON/YAML or file path)", metavar="AUTH")
configcrud.add_argument("--theme", dest="theme", nargs=1, action=self._store_type, help="Set application theme (dark, light, or YAML file path)", metavar="THEME") configcrud.add_argument("--theme", dest="theme", nargs=1, action=self._store_type, help="Set application theme (dark, light, or YAML file path)", metavar="THEME")
configcrud.add_argument("--service-mode", dest="service_mode", nargs=1, action=self._store_type, help="Set the backend service mode (local or remote)", choices=["local", "remote"]) configcrud.add_argument("--service-mode", dest="service_mode", nargs=1, action=self._store_type, help="Set the backend service mode (local or remote)", choices=["local", "remote"])
configcrud.add_argument("--remote", dest="remote_host", nargs=1, action=self._store_type, help="Connect to a remote connpy service via gRPC", metavar="HOST:PORT") configcrud.add_argument("--remote", dest="remote_host", nargs=1, action=self._store_type, help="Connect to a remote connpy service via gRPC", metavar="HOST:PORT")
configcrud.add_argument("--architect-model", dest="architect_model", nargs=1, action=self._store_type, help="Set architect model", metavar="MODEL") configcrud.add_argument("--architect-model", dest="architect_model", nargs=1, action=self._store_type, help="Set architect model", metavar="MODEL")
configcrud.add_argument("--architect-api-key", dest="architect_api_key", nargs=1, action=self._store_type, help="Set architect api_key", metavar="API_KEY") configcrud.add_argument("--architect-api-key", dest="architect_api_key", nargs=1, action=self._store_type, help="Set architect api_key", metavar="API_KEY")
configcrud.add_argument("--architect-auth", dest="architect_auth", nargs=1, action=self._store_type, help="Set architect auth (inline JSON/YAML or file path)", metavar="AUTH")
configcrud.add_argument("--sync-remote", dest="sync_remote", nargs=1, action=self._store_type, help="Sync remote nodes to Google Drive", choices=["true","false"]) configcrud.add_argument("--sync-remote", dest="sync_remote", nargs=1, action=self._store_type, help="Sync remote nodes to Google Drive", choices=["true","false"])
configparser.add_argument("--trusted-commands", dest="trusted_commands", nargs=1, action=self._store_type, help="Set custom trusted commands regexes (comma separated)", metavar="REGEX,REGEX") configparser.add_argument("--trusted-commands", dest="trusted_commands", nargs=1, action=self._store_type, help="Set custom trusted commands regexes (comma separated)", metavar="REGEX,REGEX")
configparser.set_defaults(func=self._config.dispatch) configparser.set_defaults(func=self._config.dispatch)
+24 -6
View File
@@ -315,8 +315,11 @@ class node:
def _setup_interact_environment(self, debug=False, logger=None, async_mode=False): def _setup_interact_environment(self, debug=False, logger=None, async_mode=False):
size = re.search('columns=([0-9]+).*lines=([0-9]+)',str(os.get_terminal_size())) try:
self.child.setwinsize(int(size.group(2)),int(size.group(1))) size = re.search('columns=([0-9]+).*lines=([0-9]+)',str(os.get_terminal_size()))
self.child.setwinsize(int(size.group(2)),int(size.group(1)))
except OSError:
pass
if logger: if logger:
port_str = f":{self.port}" if self.port and self.protocol not in ["ssm", "kubectl", "docker"] else "" port_str = f":{self.port}" if self.port and self.protocol not in ["ssm", "kubectl", "docker"] else ""
logger("success", f"Connected to {self.unique} at {self.host}{port_str} via: {self.protocol}") logger("success", f"Connected to {self.unique} at {self.host}{port_str} via: {self.protocol}")
@@ -353,6 +356,7 @@ class node:
async def _async_interact_loop(self, local_stream, resize_callback, copilot_handler=None): async def _async_interact_loop(self, local_stream, resize_callback, copilot_handler=None):
local_stream.setup(resize_callback=resize_callback) local_stream.setup(resize_callback=resize_callback)
self.current_local_stream = local_stream
try: try:
child_fd = self.child.child_fd child_fd = self.child.child_fd
@@ -435,11 +439,19 @@ class node:
# Remove any stray \x00 bytes and forward normally # Remove any stray \x00 bytes and forward normally
clean_data = data.replace(b'\x00', b'') clean_data = data.replace(b'\x00', b'')
if clean_data: if clean_data:
# Track command boundaries when user hits Enter # Track command boundaries when user hits Enter or presses Ctrl+C
if hasattr(self, 'mylog') and (b'\r' in clean_data or b'\n' in clean_data): if hasattr(self, 'mylog') and (b'\r' in clean_data or b'\n' in clean_data or b'\x03' in clean_data):
self.cmd_byte_positions.append((self.mylog.tell(), None)) pos = self.mylog.tell()
marker_cmd = "CANCELLED" if b'\x03' in clean_data else None
self.cmd_byte_positions.append((pos, marker_cmd))
if hasattr(self, 'current_local_stream') and self.current_local_stream is not None:
try:
await self.current_local_stream.write(f'\x1b]133;B;{pos}\x07'.encode())
except Exception:
pass
try: os.write(child_fd, clean_data) try:
os.write(child_fd, clean_data)
except OSError: except OSError:
break break
self.lastinput = time() self.lastinput = time()
@@ -559,6 +571,7 @@ class node:
except Exception: except Exception:
pass pass
finally: finally:
self.current_local_stream = None
local_stream.teardown() local_stream.teardown()
@MethodHook @MethodHook
@@ -587,6 +600,11 @@ class node:
if cmd != slc and hasattr(self, 'cmd_byte_positions') and self.cmd_byte_positions is not None: if cmd != slc and hasattr(self, 'cmd_byte_positions') and self.cmd_byte_positions is not None:
log_pos = self.mylog.tell() if hasattr(self, 'mylog') else 0 log_pos = self.mylog.tell() if hasattr(self, 'mylog') else 0
self.cmd_byte_positions.append((log_pos, cmd)) self.cmd_byte_positions.append((log_pos, cmd))
if hasattr(self, 'current_local_stream') and self.current_local_stream is not None:
try:
await self.current_local_stream.write(f'\x1b]133;B;{log_pos}\x07'.encode())
except Exception:
pass
# Write physically to PTY # Write physically to PTY
os.write(child_fd, (cmd + "\n").encode()) os.write(child_fd, (cmd + "\n").encode())
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
+26 -4
View File
@@ -375,7 +375,9 @@ class NodeServicer(connpy_pb2_grpc.NodeServiceServicer):
asyncio.run(n._async_interact_loop(remote_stream, resize_callback, copilot_handler=remote_copilot_handler)) asyncio.run(n._async_interact_loop(remote_stream, resize_callback, copilot_handler=remote_copilot_handler))
except Exception as e: except Exception as e:
pass import traceback
print(f"[ERROR in run_async_loop] {e}")
traceback.print_exc()
finally: finally:
n._teardown_interact_environment() n._teardown_interact_environment()
response_queue.put(None) # Signal EOF response_queue.put(None) # Signal EOF
@@ -481,6 +483,7 @@ class ProfileServicer(connpy_pb2_grpc.ProfileServiceServicer):
self.service = ProfileService(config) self.service = ProfileService(config)
self.node_service = NodeService(config) self.node_service = NodeService(config)
@handle_errors @handle_errors
def list_profiles(self, request, context): def list_profiles(self, request, context):
f = request.filter_str if request.filter_str else None f = request.filter_str if request.filter_str else None
@@ -729,6 +732,7 @@ class StatusBridge:
self.on_interrupt = self._force_interrupt self.on_interrupt = self._force_interrupt
self.thread = None self.thread = None
self.is_web = is_web self.is_web = is_web
self.is_remote = True
def _force_interrupt(self): def _force_interrupt(self):
"""Forcefully raise KeyboardInterrupt in the target thread.""" """Forcefully raise KeyboardInterrupt in the target thread."""
@@ -860,9 +864,11 @@ class AIServicer(connpy_pb2_grpc.AIServiceServicer):
print(f"AI Task Error: {e}") print(f"AI Task Error: {e}")
traceback.print_exc() traceback.print_exc()
chunk_queue.put(("status", f"Error: {str(e)}")) chunk_queue.put(("status", f"Error: {str(e)}"))
# Crucial: always send final_mark to avoid client deadlock
chunk_queue.put(("final_mark", {"response": f"Error: {str(e)}", "chat_history": history, "error": True}))
def request_listener(): def request_listener():
nonlocal bridge, is_web, ai_thread, agent_instance nonlocal bridge, is_web, ai_thread, agent_instance, history
try: try:
for req in request_iterator: for req in request_iterator:
if req.interrupt: if req.interrupt:
@@ -876,12 +882,21 @@ class AIServicer(connpy_pb2_grpc.AIServiceServicer):
if req.input_text: if req.input_text:
is_web = "web" in (req.session_id or "").lower() or (req.session_id or "").lower().startswith("ws-") is_web = "web" in (req.session_id or "").lower() or (req.session_id or "").lower().startswith("ws-")
# Hydrate history from client if it's the first interaction in this stream
if not history and req.chat_history:
from .utils import from_value
history = from_value(req.chat_history) or []
if not bridge: if not bridge:
bridge = StatusBridge(chunk_queue, request_queue=request_queue, is_web=is_web) bridge = StatusBridge(chunk_queue, request_queue=request_queue, is_web=is_web)
overrides = {} overrides = {}
if req.engineer_model: overrides["engineer_model"] = req.engineer_model if req.engineer_model: overrides["engineer_model"] = req.engineer_model
if req.engineer_api_key: overrides["engineer_api_key"] = req.engineer_api_key if req.engineer_api_key: overrides["engineer_api_key"] = req.engineer_api_key
if req.architect_model: overrides["architect_model"] = req.architect_model
if req.architect_api_key: overrides["architect_api_key"] = req.architect_api_key
if req.HasField("engineer_auth"): overrides["engineer_auth"] = from_struct(req.engineer_auth)
if req.HasField("architect_auth"): overrides["architect_auth"] = from_struct(req.architect_auth)
# Start AI in its own thread so we can keep listening for interrupts # Start AI in its own thread so we can keep listening for interrupts
ai_thread = threading.Thread( ai_thread = threading.Thread(
@@ -946,7 +961,8 @@ class AIServicer(connpy_pb2_grpc.AIServiceServicer):
@handle_errors @handle_errors
def list_sessions(self, request, context): def list_sessions(self, request, context):
return connpy_pb2.ValueResponse(data=to_value(self.service.list_sessions())) sessions, total = self.service.list_sessions()
return connpy_pb2.ValueResponse(data=to_value(sessions))
@handle_errors @handle_errors
def delete_session(self, request, context): def delete_session(self, request, context):
@@ -955,7 +971,8 @@ class AIServicer(connpy_pb2_grpc.AIServiceServicer):
@handle_errors @handle_errors
def configure_provider(self, request, context): def configure_provider(self, request, context):
self.service.configure_provider(request.provider, request.model, request.api_key) auth_dict = from_struct(request.auth) if request.HasField("auth") else None
self.service.configure_provider(request.provider, request.model, request.api_key, auth=auth_dict)
return Empty() return Empty()
@handle_errors @handle_errors
@@ -969,6 +986,11 @@ class AIServicer(connpy_pb2_grpc.AIServiceServicer):
) )
return Empty() return Empty()
@handle_errors
def list_mcp_servers(self, request, context):
mcp_servers = self.service.list_mcp_servers()
return connpy_pb2.ValueResponse(data=to_value(mcp_servers))
@handle_errors @handle_errors
def load_session_data(self, request, context): def load_session_data(self, request, context):
return connpy_pb2.StructResponse(data=to_struct(self.service.load_session_data(request.value))) return connpy_pb2.StructResponse(data=to_struct(self.service.load_session_data(request.value)))
+27 -4
View File
@@ -745,6 +745,10 @@ class AIStub:
) )
if chat_history is not None: if chat_history is not None:
initial_req.chat_history.CopyFrom(to_value(chat_history)) initial_req.chat_history.CopyFrom(to_value(chat_history))
if "engineer_auth" in overrides and overrides["engineer_auth"]:
initial_req.engineer_auth.CopyFrom(to_struct(overrides["engineer_auth"]))
if "architect_auth" in overrides and overrides["architect_auth"]:
initial_req.architect_auth.CopyFrom(to_struct(overrides["architect_auth"]))
req_queue.put(initial_req) req_queue.put(initial_req)
@@ -758,6 +762,7 @@ class AIStub:
full_content = "" full_content = ""
header_printed = False header_printed = False
current_responder = "engineer"
final_result = {"response": "", "chat_history": []} final_result = {"response": "", "chat_history": []}
# Background thread to pull responses from gRPC into a local queue # Background thread to pull responses from gRPC into a local queue
@@ -802,6 +807,10 @@ class AIStub:
break break
if response.status_update: if response.status_update:
if response.status_update.startswith("__RESPONDER__:"):
current_responder = response.status_update.split(":")[1].lower()
continue
if response.requires_confirmation: if response.requires_confirmation:
if status: status.stop() if status: status.stop()
@@ -854,7 +863,9 @@ class AIStub:
stable_console = RichConsole(theme=connpy_theme, file=get_original_stdout()) stable_console = RichConsole(theme=connpy_theme, file=get_original_stdout())
# Print header on first chunk # Print header on first chunk
stable_console.print(Rule("[bold engineer]Network Engineer[/bold engineer]", style="engineer")) alias = "architect" if current_responder == "architect" else "engineer"
role_label = "Network Architect" if current_responder == "architect" else "Network Engineer"
stable_console.print(Rule(f"[bold {alias}]{role_label}[/bold {alias}]", style=alias))
header_printed = True header_printed = True
# Initialize parser # Initialize parser
@@ -906,16 +917,23 @@ class AIStub:
return self.stub.confirm(connpy_pb2.StringRequest(value=input_text)).value return self.stub.confirm(connpy_pb2.StringRequest(value=input_text)).value
@handle_errors @handle_errors
def list_sessions(self): def list_sessions(self, limit=None):
return from_value(self.stub.list_sessions(Empty()).data) from .utils import from_value
res = self.stub.list_sessions(Empty())
sessions = from_value(res.data) or []
if limit and len(sessions) > limit:
return sessions[:limit], len(sessions)
return sessions, len(sessions)
@handle_errors @handle_errors
def delete_session(self, session_id): def delete_session(self, session_id):
self.stub.delete_session(connpy_pb2.StringRequest(value=session_id)) self.stub.delete_session(connpy_pb2.StringRequest(value=session_id))
@handle_errors @handle_errors
def configure_provider(self, provider, model=None, api_key=None): def configure_provider(self, provider, model=None, api_key=None, auth=None):
req = connpy_pb2.ProviderRequest(provider=provider, model=model or "", api_key=api_key or "") req = connpy_pb2.ProviderRequest(provider=provider, model=model or "", api_key=api_key or "")
if auth:
req.auth.CopyFrom(to_struct(auth))
self.stub.configure_provider(req) self.stub.configure_provider(req)
@handle_errors @handle_errors
@@ -929,6 +947,11 @@ class AIStub:
) )
self.stub.configure_mcp(req) self.stub.configure_mcp(req)
@handle_errors
def list_mcp_servers(self):
res = self.stub.list_mcp_servers(Empty())
return from_value(res.data) or {}
@handle_errors @handle_errors
def load_session_data(self, session_id): def load_session_data(self, session_id):
return from_struct(self.stub.load_session_data(connpy_pb2.StringRequest(value=session_id)).data) return from_struct(self.stub.load_session_data(connpy_pb2.StringRequest(value=session_id)).data)
+4
View File
@@ -70,6 +70,7 @@ service AIService {
rpc delete_session (StringRequest) returns (google.protobuf.Empty) {} rpc delete_session (StringRequest) returns (google.protobuf.Empty) {}
rpc configure_provider (ProviderRequest) returns (google.protobuf.Empty) {} rpc configure_provider (ProviderRequest) returns (google.protobuf.Empty) {}
rpc configure_mcp (MCPRequest) returns (google.protobuf.Empty) {} rpc configure_mcp (MCPRequest) returns (google.protobuf.Empty) {}
rpc list_mcp_servers (google.protobuf.Empty) returns (ValueResponse) {}
rpc load_session_data (StringRequest) returns (StructResponse) {} rpc load_session_data (StringRequest) returns (StructResponse) {}
} }
@@ -234,6 +235,8 @@ message AskRequest {
bool trust = 10; bool trust = 10;
string confirmation_answer = 11; string confirmation_answer = 11;
bool interrupt = 12; bool interrupt = 12;
google.protobuf.Struct engineer_auth = 13;
google.protobuf.Struct architect_auth = 14;
} }
message AIResponse { message AIResponse {
@@ -254,6 +257,7 @@ message ProviderRequest {
string provider = 1; string provider = 1;
string model = 2; string model = 2;
string api_key = 3; string api_key = 3;
google.protobuf.Struct auth = 4;
} }
message IntRequest { message IntRequest {
+107 -25
View File
@@ -6,6 +6,37 @@ from connpy.utils import log_cleaner
class AIService(BaseService): class AIService(BaseService):
"""Business logic for interacting with AI agents and LLM configurations.""" """Business logic for interacting with AI agents and LLM configurations."""
def _clean_cisco_scrolling(self, text: str) -> str:
"""Resolves horizontal scrolling artifacts (backspaces, \r, ANSI) by merging overlapping segments."""
def merge_overlapping(s1, s2):
s2_clean = s2.lstrip(' $')
max_overlap = min(len(s1), len(s2_clean))
for i in range(max_overlap, 0, -1):
if s1[-i:] == s2_clean[:i]:
return s1 + s2_clean[i:]
return s1 + s2_clean
scroll_re = re.compile(r'(\x08{5,}\s*\$?|\$\r|\x1b\[\d+[GD]\s*\$?)')
parts = scroll_re.split(text)
merged = ""
for part in parts:
if scroll_re.match(part):
continue
cleaned = log_cleaner(part)
if not merged:
merged = cleaned
else:
merged_lines = merged.split('\n')
cleaned_lines = cleaned.split('\n')
merged_lines[-1] = merge_overlapping(merged_lines[-1], cleaned_lines[0])
merged_lines.extend(cleaned_lines[1:])
merged = "\n".join(merged_lines)
return merged
def build_context_blocks(self, raw_bytes: bytes, cmd_byte_positions: list, node_info: dict, last_line: str = "") -> list: def build_context_blocks(self, raw_bytes: bytes, cmd_byte_positions: list, node_info: dict, last_line: str = "") -> list:
"""Identifies command blocks in the terminal history.""" """Identifies command blocks in the terminal history."""
blocks = [] blocks = []
@@ -27,28 +58,69 @@ class AIService(BaseService):
prev_pos = cmd_byte_positions[i-1][0] prev_pos = cmd_byte_positions[i-1][0]
if known_cmd: if known_cmd:
prev_chunk = raw_bytes[prev_pos:pos] if known_cmd == "CANCELLED":
prev_cleaned = log_cleaner(prev_chunk.decode(errors='replace')) parsed_positions.append({"pos": pos, "type": "CANCELLED", "preview": ""})
prev_lines = [l for l in prev_cleaned.split('\n') if l.strip()] else:
prompt_text = prev_lines[-1].strip() if prev_lines else "" prev_chunk = raw_bytes[prev_pos:pos]
preview = f"{prompt_text}{known_cmd}" if prompt_text else known_cmd prev_cleaned = self._clean_cisco_scrolling(prev_chunk.decode(errors='replace'))
parsed_positions.append({"pos": pos, "type": "VALID_CMD", "preview": preview[:80]}) prev_lines = [l for l in prev_cleaned.split('\n') if l.strip()]
prompt_text = prev_lines[-1].strip() if prev_lines else ""
preview = f"{prompt_text}{known_cmd}" if prompt_text else known_cmd
if len(preview) > 80:
preview = preview[:77] + "..."
parsed_positions.append({"pos": pos, "type": "VALID_CMD", "preview": preview})
else: else:
chunk = raw_bytes[prev_pos:pos] chunk = raw_bytes[prev_pos:pos]
cleaned = log_cleaner(chunk.decode(errors='replace'))
lines = [l for l in cleaned.split('\n') if l.strip()]
preview = lines[-1].strip() if lines else ""
if preview: cleaned = self._clean_cisco_scrolling(chunk.decode(errors='replace'))
match = prompt_re.search(preview) lines = [l for l in cleaned.split('\n') if l.strip()]
if match:
cmd_text = preview[match.end():].strip() found_in_pass1 = False
if cmd_text: if lines:
parsed_positions.append({"pos": pos, "type": "VALID_CMD", "preview": preview[:80]}) # Search backwards through the last few lines for the prompt
else: for idx in range(len(lines) - 1, max(-1, len(lines) - 10), -1):
parsed_positions.append({"pos": pos, "type": "EMPTY_PROMPT", "preview": ""}) match = prompt_re.search(lines[idx])
else: if match:
parsed_positions.append({"pos": pos, "type": "SCROLLING", "preview": ""}) ptxt = match.group(0).strip()
cmd_first_line = lines[idx][match.end():].strip()
cmd_rest = [l.strip() for l in lines[idx+1:]]
cmd_text = " ".join([cmd_first_line] + cmd_rest).strip()
if cmd_text:
pv = f"{ptxt} {cmd_text}".strip()
if len(pv) > 80:
pv = pv[:77] + "..."
parsed_positions.append({"pos": pos, "type": "VALID_CMD", "preview": pv})
else:
parsed_positions.append({"pos": pos, "type": "EMPTY_PROMPT", "preview": ""})
found_in_pass1 = True
break
if not found_in_pass1:
# Fallback: The prompt might have been isolated in the previous chunk
# due to asynchronous network delays splitting the output exactly at the newline.
prev_was_valid_cmd = i >= 2 and parsed_positions[i-2]["type"] == "VALID_CMD"
if prev_pos > 0 and not prev_was_valid_cmd:
# Fetch the very last chunk that we just processed
prev_prev_pos = cmd_byte_positions[i-2][0] if i >= 2 else 0
prev_chunk_text = self._clean_cisco_scrolling(raw_bytes[prev_prev_pos:prev_pos].decode(errors='replace'))
prev_lines_text = [l for l in prev_chunk_text.split('\n') if l.strip()]
if prev_lines_text:
prev_match = prompt_re.search(prev_lines_text[-1])
if prev_match:
ptxt = prev_match.group(0).strip()
cmd_text = " ".join([l.strip() for l in lines]).strip()
if cmd_text:
pv = f"{ptxt} {cmd_text}".strip()
if len(pv) > 80:
pv = pv[:77] + "..."
parsed_positions.append({"pos": pos, "type": "VALID_CMD", "preview": pv})
found_in_pass1 = True
if not found_in_pass1:
parsed_positions.append({"pos": pos, "type": "SCROLLING", "preview": ""})
else: else:
parsed_positions.append({"pos": pos, "type": "SCROLLING", "preview": ""}) parsed_positions.append({"pos": pos, "type": "SCROLLING", "preview": ""})
@@ -61,11 +133,11 @@ class AIService(BaseService):
start_pos = item["pos"] start_pos = item["pos"]
preview = item["preview"] preview = item["preview"]
# Find the end position: next VALID_CMD or EMPTY_PROMPT # Find the end position: next VALID_CMD or EMPTY_PROMPT or CANCELLED
end_pos = current_prompt_pos end_pos = current_prompt_pos
for j in range(i + 1, len(parsed_positions)): for j in range(i + 1, len(parsed_positions)):
next_item = parsed_positions[j] next_item = parsed_positions[j]
if next_item["type"] in ("VALID_CMD", "EMPTY_PROMPT"): if next_item["type"] in ("VALID_CMD", "EMPTY_PROMPT", "CANCELLED"):
end_pos = next_item["pos"] end_pos = next_item["pos"]
break break
@@ -167,11 +239,14 @@ class AIService(BaseService):
return await asyncio.wrap_future(future) return await asyncio.wrap_future(future)
def list_sessions(self): def list_sessions(self, limit=None):
"""Return a list of all saved AI sessions.""" """Return a list of saved AI sessions, optionally limited."""
from connpy.ai import ai from connpy.ai import ai
agent = ai(self.config) agent = ai(self.config)
return agent._get_sessions() sessions = agent._get_sessions()
if limit and len(sessions) > limit:
return sessions[:limit], len(sessions)
return sessions, len(sessions)
def delete_session(self, session_id): def delete_session(self, session_id):
"""Delete an AI session by ID.""" """Delete an AI session by ID."""
@@ -183,13 +258,15 @@ class AIService(BaseService):
else: else:
raise InvalidConfigurationError(f"Session '{session_id}' not found.") raise InvalidConfigurationError(f"Session '{session_id}' not found.")
def configure_provider(self, provider, model=None, api_key=None): def configure_provider(self, provider, model=None, api_key=None, auth=None):
"""Update AI provider settings in the configuration.""" """Update AI provider settings in the configuration."""
settings = self.config.config.get("ai", {}) settings = self.config.config.get("ai", {})
if model: if model:
settings[f"{provider}_model"] = model settings[f"{provider}_model"] = model
if api_key: if api_key:
settings[f"{provider}_api_key"] = api_key settings[f"{provider}_api_key"] = api_key
if auth is not None:
settings[f"{provider}_auth"] = auth
self.config.config["ai"] = settings self.config.config["ai"] = settings
self.config._saveconfig(self.config.file) self.config._saveconfig(self.config.file)
@@ -228,6 +305,11 @@ class AIService(BaseService):
self.config.config["ai"] = ai_settings self.config.config["ai"] = ai_settings
self.config._saveconfig(self.config.file) self.config._saveconfig(self.config.file)
def list_mcp_servers(self) -> dict:
"""Get the configured MCP servers."""
ai_settings = self.config.config.get("ai", {})
return ai_settings.get("mcp_servers", {})
def load_session_data(self, session_id): def load_session_data(self, session_id):
"""Load a session's raw data by ID.""" """Load a session's raw data by ID."""
from connpy.ai import ai from connpy.ai import ai
+76 -3
View File
@@ -23,7 +23,7 @@ class TestAIInit:
myai = ai(config) myai = ai(config)
with pytest.raises(ValueError) as exc: with pytest.raises(ValueError) as exc:
myai.ask("hello") myai.ask("hello")
assert "Engineer API key not configured" in str(exc.value) assert "Engineer API key or authentication not configured" in str(exc.value)
def test_init_missing_architect_key_warns(self, ai_config, capsys, mock_litellm): def test_init_missing_architect_key_warns(self, ai_config, capsys, mock_litellm):
"""Warns if architect key is missing but doesn't crash.""" """Warns if architect key is missing but doesn't crash."""
@@ -58,6 +58,77 @@ class TestAIInit:
pass # May fail on other file opens, that's ok pass # May fail on other file opens, that's ok
# =========================================================================
# AI Auth Dict tests
# =========================================================================
class TestAIAuthDict:
def test_init_with_auth_dict(self, ai_config):
"""Initializes correctly when auth dicts are configured."""
from connpy.ai import ai
ai_config.config["ai"]["engineer_api_key"] = None
ai_config.config["ai"]["architect_api_key"] = None
ai_config.config["ai"]["engineer_auth"] = {"my_key": "my_val"}
ai_config.config["ai"]["architect_auth"] = {"another_key": "another_val"}
myai = ai(ai_config)
assert myai.engineer_auth == {"my_key": "my_val"}
assert myai.architect_auth == {"another_key": "another_val"}
def test_compat_key_injection(self, ai_config):
"""Injects API key into auth dict if auth is empty or doesn't have it."""
from connpy.ai import ai
ai_config.config["ai"]["engineer_api_key"] = "compat-eng-key"
ai_config.config["ai"]["architect_api_key"] = "compat-arch-key"
ai_config.config["ai"]["engineer_auth"] = {}
ai_config.config["ai"]["architect_auth"] = {}
myai = ai(ai_config)
assert myai.engineer_auth == {"api_key": "compat-eng-key"}
assert myai.architect_auth == {"api_key": "compat-arch-key"}
def test_has_architect_keyless(self, ai_config):
"""Evaluates has_architect correctly for keyless models and auth configs."""
from connpy.ai import ai
# 1. Keyless model (Vertex)
ai_config.config["ai"]["architect_api_key"] = None
ai_config.config["ai"]["architect_auth"] = {}
ai_config.config["ai"]["architect_model"] = "vertex/gemini-pro"
myai = ai(ai_config)
assert myai.has_architect is True
# 2. Architect auth dict is set
ai_config.config["ai"]["architect_model"] = "custom-model"
ai_config.config["ai"]["architect_auth"] = {"vertex_project": "proj-1"}
myai = ai(ai_config)
assert myai.has_architect is True
def test_ask_unpacks_auth_dict(self, ai_config, mock_litellm):
"""Verifies that ask unpacks engineer_auth when calling completion."""
from connpy.ai import ai
ai_config.config["ai"]["engineer_api_key"] = None
ai_config.config["ai"]["engineer_auth"] = {"vertex_project": "my-project", "vertex_location": "us-east1"}
myai = ai(ai_config)
myai.ask("test query", stream=False)
# Check mock_litellm completion call
mock_litellm["completion"].assert_called()
kwargs = mock_litellm["completion"].call_args.kwargs
assert kwargs.get("vertex_project") == "my-project"
assert kwargs.get("vertex_location") == "us-east1"
assert "api_key" not in kwargs
def test_auth_precedence_no_api_key_injection(self, ai_config):
"""Verifies that api_key is not injected into the auth dict when auth is already set (non-empty)."""
from connpy.ai import ai
ai_config.config["ai"]["engineer_api_key"] = "legacy-eng-key"
ai_config.config["ai"]["architect_api_key"] = "legacy-arch-key"
ai_config.config["ai"]["engineer_auth"] = {"vertex_project": "proj-eng"}
ai_config.config["ai"]["architect_auth"] = {"vertex_project": "proj-arch"}
myai = ai(ai_config)
assert myai.engineer_auth == {"vertex_project": "proj-eng"}
assert "api_key" not in myai.engineer_auth
assert myai.architect_auth == {"vertex_project": "proj-arch"}
assert "api_key" not in myai.architect_auth
# ========================================================================= # =========================================================================
# register_ai_tool tests # register_ai_tool tests
# ========================================================================= # =========================================================================
@@ -427,12 +498,14 @@ class TestAISessions:
def test_generate_session_id(self, myai): def test_generate_session_id(self, myai):
session_id = myai._generate_session_id("Any query") session_id = myai._generate_session_id("Any query")
# Format: YYYYMMDD-HHMMSS # Format: YYYYMMDD-HHMMSS-suffix
assert len(session_id) == 15 assert len(session_id) == 20
assert "-" in session_id assert "-" in session_id
parts = session_id.split("-") parts = session_id.split("-")
assert len(parts) == 3
assert len(parts[0]) == 8 # YYYYMMDD assert len(parts[0]) == 8 # YYYYMMDD
assert len(parts[1]) == 6 # HHMMSS assert len(parts[1]) == 6 # HHMMSS
assert len(parts[2]) == 4 # suffix
def test_save_and_load_session(self, myai): def test_save_and_load_session(self, myai):
history = [ history = [
+242
View File
@@ -158,3 +158,245 @@ def test_ingress_task_interception():
assert called_copilot assert called_copilot
asyncio.run(run_test()) asyncio.run(run_test())
def test_build_context_blocks_horizontal_scrolling():
from connpy.services.ai_service import AIService
svc = AIService(None)
node_info = {"prompt": "RP/0/RP0/CPU0:xrd#"}
part1 = 'RP/0/RP0/CPU0:xrd#s show interfaces * | inc "rate|is up|escr|test1|test2|test3|test4|test5|teest8|test7|t$'
part2 = '|escr|test1|test2|test3|test4|test5|teest8|test7|te s998"show interfaces * | inc "rate|is up|escr|test1|test2|test3|test4|test5|teest8|test7|$'
# Test with \r (classic IOS)
raw_bytes = (part1 + '\r' + part2).encode()
cmd_byte_positions = [(0, None), (len(raw_bytes), None)]
blocks = svc.build_context_blocks(raw_bytes, cmd_byte_positions, node_info)
assert len(blocks) >= 1
start, end, preview = blocks[0]
assert "RP/0/RP0/CPU0:xrd# s show interfaces * | inc" in preview
def test_build_context_blocks_horizontal_scrolling_ansi():
"""Test with CSI cursor repositioning (\\x1B[1G) instead of raw \\r, as used by Cisco IOS XR."""
from connpy.services.ai_service import AIService
svc = AIService(None)
node_info = {"prompt": "RP/0/RP0/CPU0:xrd#"}
part1 = 'RP/0/RP0/CPU0:xrd#s show interfaces * | inc "rate|is up|escr|test1|test2|test3|test4|test5|teest8|test7|t'
part2 = '$|escr|test1|test2|test3|test4|test5|teest8|test7|te s998"show interfaces * | inc "rate|is up|escr|test1|test2|test3|test4|test5|teest8|test7|$'
# Test with \x1B[1G (CSI Cursor Horizontal Absolute - IOS XR)
raw_bytes = (part1 + '\x1b[1G' + part2).encode()
cmd_byte_positions = [(0, None), (len(raw_bytes), None)]
blocks = svc.build_context_blocks(raw_bytes, cmd_byte_positions, node_info)
assert len(blocks) >= 1
start, end, preview = blocks[0]
assert "RP/0/RP0/CPU0:xrd# s show interfaces * | inc" in preview
def test_build_context_blocks_cancelled_command():
from connpy.services.ai_service import AIService
svc = AIService(None)
node_info = {"prompt": "router#"}
# Command 1: cancelled with Ctrl+C. Command 2: executed successfully.
raw_bytes = b"router# show plat\x03\r\nrouter# show ver\r\nrouter# "
# 0: initial boundary
# 18: Ctrl+C pressed (ends Command 1, marked CANCELLED)
# 36: Enter pressed (ends Command 2)
cmd_byte_positions = [(0, None), (18, "CANCELLED"), (36, None)]
blocks = svc.build_context_blocks(raw_bytes, cmd_byte_positions, node_info)
# The cancelled command block (0 to 18) should NOT be registered as a VALID_CMD block.
# The block for "show ver" should be registered (starting at 36, ending at current_prompt_pos).
# Plus, the final block for "CURRENT CONTEXT".
valid_blocks = [b for b in blocks if "CURRENT CONTEXT" not in b[2]]
assert len(valid_blocks) == 1
assert "show ver" in valid_blocks[0][2]
assert "show plat" not in valid_blocks[0][2]
def test_copilot_range_mode_filtering():
from connpy.cli.terminal_ui import CopilotInterface
# We setup dummy raw_bytes with scrolling garbage in the middle:
# 0 to 10: "show ip" (VALID_CMD)
# 10 to 25: "some scrolling garbage we want to skip"
# 25 to 35: "show run" (VALID_CMD)
# 35 to 45: "current prompt" (final context block)
raw_bytes = b"show ip garbage_to_skip_here show run router#"
blocks = [
(0, 10, "router# show ip"),
(25, 35, "router# show run"),
(35, 45, "router#")
]
# Mock Config
class MockConfig:
def __init__(self):
self.config = {"ai": {}}
self.defaultdir = "/tmp"
interface = CopilotInterface(MockConfig())
# Ensure default is RANGE mode
interface.mode_range = 0
interface.mode_single = 1
interface.mode_lines = 2
captured_buffer = None
async def mock_ai_call(active_buffer, question, on_chunk, node_info):
nonlocal captured_buffer
captured_buffer = active_buffer
return {"guide": "Ok", "commands": [], "risk_level": "low"}
# Mock PromptSession.prompt_async to ask a question once then exit
prompt_calls = 0
async def mock_prompt_async(self, *args, **kwargs):
nonlocal prompt_calls
prompt_calls += 1
if prompt_calls == 1:
# Simulate pressing Ctrl+Up key twice to expand context range from 1 to 3 commands
kb = kwargs.get('key_bindings')
if kb:
class DummyApp:
def invalidate(self): pass
class DummyEvent:
app = DummyApp()
# Find and invoke the 'c-up' handler twice
for b in kb.bindings:
if any('up' in str(k).lower() for k in b.keys):
b.handler(DummyEvent())
b.handler(DummyEvent())
return "how are interfaces looking?"
else:
raise KeyboardInterrupt
with patch('prompt_toolkit.PromptSession.prompt_async', mock_prompt_async):
async def run():
# Run session
return await interface.run_session(
raw_bytes=raw_bytes,
node_info={"name": "test"},
on_ai_call=mock_ai_call,
blocks=blocks
)
asyncio.run(run())
# In range mode: it should have concatenated the valid blocks
# block[0] is raw_bytes[0:10] => b"show ip "
# block[1] is raw_bytes[25:35] => b" show run"
# block[2] is raw_bytes[35:45] => b" router#"
# Note: raw_bytes[10:25] (garbage) must be excluded!
assert captured_buffer is not None
assert "garbage_to_skip_here" not in captured_buffer
assert "show ip" in captured_buffer
assert "show run" in captured_buffer
def test_build_context_blocks_pager_scrolling_enter():
from connpy.services.ai_service import AIService
svc = AIService(None)
node_info = {"prompt": "sixwind>"}
raw_bytes = (
b"sixwind> show configuration | less\r\n"
b"line 1 of output\nline 2 of output\n\r"
b"line 3 of output\nline 4 of output\n\r"
b"line 5 of output\n(END)\x1b[?1049l\x1b[?47l\r\nsixwind> \r\n"
b"sixwind> \r\n"
b"sixwind> \r\n"
b"sixwind> "
)
cmd_byte_positions = [
(0, None),
(36, None),
(70, None),
(105, None),
(153, None),
(164, None),
(175, None),
(186, None)
]
blocks = svc.build_context_blocks(raw_bytes, cmd_byte_positions, node_info)
valid_blocks = [b for b in blocks if "CURRENT CONTEXT" not in b[2]]
assert len(valid_blocks) == 1
assert "show configuration" in valid_blocks[0][2]
assert valid_blocks[0][0] == 36
assert valid_blocks[0][1] == 153
def test_build_context_blocks_pager_scrolling_space():
from connpy.services.ai_service import AIService
svc = AIService(None)
node_info = {"prompt": "sixwind>"}
raw_bytes = (
b"sixwind> show configuration | less\r\n"
b"line 1 of output\nline 2 of output\n "
b"line 3 of output\nline 4 of output\n "
b"line 5 of output\n(END)\x1b[?1049l\x1b[?47l\r\n"
b"sixwind> \r\n"
b"sixwind> \r\n"
b"sixwind> \r\n"
b"sixwind> "
)
cmd_byte_positions = [
(0, None),
(36, None),
(144, None),
(155, None),
(166, None),
(177, None)
]
blocks = svc.build_context_blocks(raw_bytes, cmd_byte_positions, node_info)
valid_blocks = [b for b in blocks if "CURRENT CONTEXT" not in b[2]]
assert len(valid_blocks) == 1
assert "show configuration" in valid_blocks[0][2]
assert valid_blocks[0][0] == 36
assert valid_blocks[0][1] == 155
def test_build_context_blocks_pager_scrolling_6wind_escapes():
from connpy.services.ai_service import AIService
svc = AIService(None)
node_info = {"prompt": "6WIND-PE1>", "os": "6wind"}
raw_bytes = (
b"6WIND-PE1> show config running fullpath nodefault\r\n"
b"line 1\r\n"
b"line 2\r\n"
b":\x1b[K\r\x1b[K/ vrf main interface gre gre2 mtu 8400\r\n"
b":\x1b[K\x07\r\x1b[K\x1b[?1l\x1b>6WIND-PE1> \r\n"
b"6WIND-PE1> \r\n"
b"6WIND-PE1> "
)
cmd_byte_positions = [
(0, None),
(52, None),
(136, None),
(177, None),
(177, None),
(190, None),
(203, None)
]
blocks = svc.build_context_blocks(raw_bytes, cmd_byte_positions, node_info)
valid_blocks = [b for b in blocks if "CURRENT CONTEXT" not in b[2]]
assert len(valid_blocks) == 1
assert "show config running" in valid_blocks[0][2]
+76
View File
@@ -65,4 +65,80 @@ class TestGetCwd:
assert len(dirs_in_result) > 0 assert len(dirs_in_result) > 0
# =========================================================================
# Tree completions tests
# =========================================================================
class TestTreeCompletions:
def test_config_auth_completions(self):
from connpy.completion import _build_tree, resolve_completion
tree = _build_tree([], [], [], {}, "/tmp")
# Test config completions
config_completions = resolve_completion(["config", ""], tree)
assert "--engineer-auth" in config_completions
assert "--architect-auth" in config_completions
# Resolve when --engineer-auth is chosen in config
auth_comp = resolve_completion(["config", "--engineer-auth", ""], tree)
assert isinstance(auth_comp, list)
# Loop back check:
# e.g., connpy config --engineer-auth some_val
# should loop back and resolve to config options
loop_back_comp = resolve_completion(["config", "--engineer-auth", "some_val", ""], tree)
assert "--architect-auth" in loop_back_comp
assert "--engineer-auth" in loop_back_comp
def test_ai_auth_completions(self):
from connpy.completion import _build_tree, resolve_completion
tree = _build_tree([], [], [], {}, "/tmp")
# Test ai completions
ai_completions = resolve_completion(["ai", ""], tree)
assert "--engineer-auth" in ai_completions
assert "--architect-auth" in ai_completions
# Resolve after choosing option
auth_comp = resolve_completion(["ai", "--engineer-auth", ""], tree)
assert isinstance(auth_comp, list)
# Loop back check:
# e.g., connpy ai --engineer-auth some_val
# should loop back and resolve to ai options, excluding --engineer-auth
loop_back_comp = resolve_completion(["ai", "--engineer-auth", "some_val", ""], tree)
assert "--architect-auth" in loop_back_comp
assert "--engineer-auth" not in loop_back_comp
def test_sixwindmcp_plugin_completions(self):
from connpy.completion import resolve_completion, get_cwd
import importlib.util
# Load the testremote/remote_plugins/sixwindmcp.py plugin
plugin_path = os.path.join(
os.path.dirname(os.path.dirname(os.path.dirname(__file__))),
"testremote", "remote_plugins", "sixwindmcp.py"
)
spec = importlib.util.spec_from_file_location("sixwindmcp", plugin_path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
module.get_cwd = get_cwd
plugin_node = module._connpy_tree()
assert "--set-path" in plugin_node
assert "--path" in plugin_node
assert "start" in plugin_node
tree = {"sixwindmcp": plugin_node}
# Test resolution when --set-path is chosen
res = resolve_completion(["sixwindmcp", "--set-path", ""], tree)
assert isinstance(res, list)
# Loop back check:
# e.g., connpy sixwindmcp --set-path /tmp start
# should loop back and resolve to plugin options
loop_back_comp = resolve_completion(["sixwindmcp", "--set-path", "/tmp", ""], tree)
assert "start" in loop_back_comp
assert "stop" in loop_back_comp
+53 -1
View File
@@ -246,7 +246,7 @@ def test_plugin_disable(mock_disable, app):
@patch("connpy.services.ai_service.AIService.list_sessions") @patch("connpy.services.ai_service.AIService.list_sessions")
def test_ai_list(mock_list_sessions, app): def test_ai_list(mock_list_sessions, app):
mock_list_sessions.return_value = [{"id": "1", "title": "t", "created_at": "now", "model": "m"}] mock_list_sessions.return_value = ([{"id": "1", "title": "t", "created_at": "now", "model": "m"}], 1)
app.start(["ai", "--list"]) app.start(["ai", "--list"])
mock_list_sessions.assert_called_once() mock_list_sessions.assert_called_once()
@@ -262,3 +262,55 @@ def test_type_node_reserved_word(app):
with pytest.raises(SystemExit) as exc: with pytest.raises(SystemExit) as exc:
app._type_node("bulk") app._type_node("bulk")
assert exc.value.code == 2 assert exc.value.code == 2
@patch("connpy.services.config_service.ConfigService.update_setting")
@patch("connpy.services.config_service.ConfigService.get_settings")
def test_config_auth_inline_json(mock_get_settings, mock_update_setting, app):
mock_get_settings.return_value = {"ai": {}}
app.start(["config", "--engineer-auth", '{"vertex_project": "test-123"}'])
mock_update_setting.assert_called_once()
args, kwargs = mock_update_setting.call_args
assert args[0] == "ai"
assert args[1]["engineer_auth"] == {"vertex_project": "test-123"}
@patch("connpy.services.config_service.ConfigService.update_setting")
@patch("connpy.services.config_service.ConfigService.get_settings")
def test_config_auth_inline_yaml(mock_get_settings, mock_update_setting, app):
mock_get_settings.return_value = {"ai": {}}
app.start(["config", "--architect-auth", 'project: test-yaml'])
mock_update_setting.assert_called_once()
args, kwargs = mock_update_setting.call_args
assert args[0] == "ai"
assert args[1]["architect_auth"] == {"project": "test-yaml"}
@patch("connpy.services.config_service.ConfigService.update_setting")
@patch("connpy.services.config_service.ConfigService.get_settings")
def test_config_clear_auth(mock_get_settings, mock_update_setting, app):
mock_get_settings.return_value = {"ai": {"engineer_auth": {"project": "123"}, "engineer_api_key": "some-key"}}
app.start(["config", "--engineer-auth", "clear"])
args, kwargs = mock_update_setting.call_args
assert "engineer_auth" not in args[1]
app.start(["config", "--engineer-api-key", "none"])
args, kwargs = mock_update_setting.call_args
assert "engineer_api_key" not in args[1]
@patch("os.path.exists")
@patch("builtins.open")
@patch("connpy.services.config_service.ConfigService.update_setting")
@patch("connpy.services.config_service.ConfigService.get_settings")
def test_config_auth_file_path(mock_get_settings, mock_update_setting, mock_open, mock_exists, app):
mock_get_settings.return_value = {"ai": {}}
mock_exists.side_effect = lambda p: True if p == "/path/to/creds.json" else False
mock_file = MagicMock()
mock_file.read.return_value = '{"vertex_project": "file-project"}'
mock_open.return_value.__enter__.return_value = mock_file
app.start(["config", "--engineer-auth", "/path/to/creds.json"])
mock_update_setting.assert_called_once()
args, kwargs = mock_update_setting.call_args
assert args[0] == "ai"
assert args[1]["engineer_auth"] == {"vertex_project": "file-project"}
+136
View File
@@ -0,0 +1,136 @@
"""
Tests for gRPC auth serialization/deserialization (engineer_auth, architect_auth, provider auth).
These tests verify that:
1. to_struct/from_struct round-trips correctly for auth dicts.
2. AIStub.ask() correctly serializes engineer_auth and architect_auth into AskRequest.
3. AIServicer.ask() correctly deserializes them and passes them to the service.
4. AIStub.configure_provider() serializes auth into ProviderRequest.
5. AIServicer.configure_provider() deserializes auth and forwards it to the service.
"""
import pytest
from unittest.mock import MagicMock, patch, call
from connpy.grpc_layer import connpy_pb2
from connpy.grpc_layer.utils import to_struct, from_struct
# --- Unit: Struct round-trip ---
class TestStructRoundTrip:
def test_simple_dict(self):
d = {"api_key": "secret", "region": "us-east-1"}
assert from_struct(to_struct(d)) == d
def test_nested_dict(self):
d = {"vertex_project": "my-project", "vertex_location": "us-central1", "nested": {"key": "val"}}
assert from_struct(to_struct(d)) == d
def test_empty_dict(self):
assert from_struct(to_struct({})) == {}
def test_none_returns_empty(self):
assert from_struct(to_struct(None)) == {}
# --- Unit: AskRequest Struct fields ---
class TestAskRequestStructFields:
def test_engineer_auth_round_trip(self):
auth = {"vertex_project": "proj", "vertex_location": "us-central1"}
req = connpy_pb2.AskRequest(input_text="hi")
req.engineer_auth.CopyFrom(to_struct(auth))
assert from_struct(req.engineer_auth) == auth
def test_architect_auth_round_trip(self):
auth = {"api_key": "sk-abc", "base_url": "https://custom.api/v1"}
req = connpy_pb2.AskRequest(input_text="hi")
req.architect_auth.CopyFrom(to_struct(auth))
assert from_struct(req.architect_auth) == auth
def test_has_field_false_when_unset(self):
req = connpy_pb2.AskRequest(input_text="hi")
assert not req.HasField("engineer_auth")
assert not req.HasField("architect_auth")
def test_has_field_true_when_set(self):
req = connpy_pb2.AskRequest(input_text="hi")
req.engineer_auth.CopyFrom(to_struct({"k": "v"}))
assert req.HasField("engineer_auth")
# --- Unit: ProviderRequest Struct field ---
class TestProviderRequestStructField:
def test_auth_round_trip(self):
auth = {"vertex_project": "proj", "vertex_location": "eu-west1"}
req = connpy_pb2.ProviderRequest(provider="vertex", model="gemini-pro")
req.auth.CopyFrom(to_struct(auth))
assert from_struct(req.auth) == auth
def test_has_field_false_when_unset(self):
req = connpy_pb2.ProviderRequest(provider="openai", model="gpt-4o")
assert not req.HasField("auth")
def test_has_field_true_when_set(self):
req = connpy_pb2.ProviderRequest(provider="vertex")
req.auth.CopyFrom(to_struct({"vertex_project": "p"}))
assert req.HasField("auth")
# --- Integration: Server deserializes auth and passes to service ---
class TestAIServicerAuthDeserialization:
@pytest.fixture
def servicer(self, populated_config):
from connpy.grpc_layer.server import AIServicer
return AIServicer(populated_config)
def test_configure_provider_passes_auth_to_service(self, servicer):
auth = {"vertex_project": "my-proj", "vertex_location": "us-central1"}
req = connpy_pb2.ProviderRequest(provider="vertex", model="gemini/gemini-pro", api_key="")
req.auth.CopyFrom(to_struct(auth))
with patch.object(servicer.service, "configure_provider") as mock_cp:
mock_context = MagicMock()
servicer.configure_provider(req, mock_context)
mock_cp.assert_called_once_with("vertex", "gemini/gemini-pro", "", auth=auth)
def test_configure_provider_no_auth(self, servicer):
req = connpy_pb2.ProviderRequest(provider="openai", model="gpt-4o", api_key="sk-test")
with patch.object(servicer.service, "configure_provider") as mock_cp:
mock_context = MagicMock()
servicer.configure_provider(req, mock_context)
mock_cp.assert_called_once_with("openai", "gpt-4o", "sk-test", auth=None)
# --- Integration: Stub serializes auth into request ---
class TestAIStubAuthSerialization:
@pytest.fixture
def ai_stub(self):
from connpy.grpc_layer.stubs import AIStub
mock_channel = MagicMock()
stub = AIStub(mock_channel, "localhost:8048")
return stub
def test_configure_provider_with_auth_serializes_struct(self, ai_stub):
auth = {"vertex_project": "proj", "vertex_location": "us-central1"}
ai_stub.stub.configure_provider = MagicMock()
ai_stub.configure_provider("vertex", model="gemini/gemini-pro", auth=auth)
ai_stub.stub.configure_provider.assert_called_once()
sent_req = ai_stub.stub.configure_provider.call_args[0][0]
assert sent_req.provider == "vertex"
assert sent_req.model == "gemini/gemini-pro"
assert sent_req.HasField("auth")
assert from_struct(sent_req.auth) == auth
def test_configure_provider_without_auth_no_struct(self, ai_stub):
ai_stub.stub.configure_provider = MagicMock()
ai_stub.configure_provider("openai", model="gpt-4o", api_key="sk-x")
sent_req = ai_stub.stub.configure_provider.call_args[0][0]
assert not sent_req.HasField("auth")
+11
View File
@@ -120,6 +120,7 @@ class TestGRPCIntegration:
connpy_pb2_grpc.add_ConfigServiceServicer_to_server(server.ConfigServicer(populated_config), srv) connpy_pb2_grpc.add_ConfigServiceServicer_to_server(server.ConfigServicer(populated_config), srv)
connpy_pb2_grpc.add_ExecutionServiceServicer_to_server(server.ExecutionServicer(populated_config), srv) connpy_pb2_grpc.add_ExecutionServiceServicer_to_server(server.ExecutionServicer(populated_config), srv)
connpy_pb2_grpc.add_ImportExportServiceServicer_to_server(server.ImportExportServicer(populated_config), srv) connpy_pb2_grpc.add_ImportExportServiceServicer_to_server(server.ImportExportServicer(populated_config), srv)
connpy_pb2_grpc.add_AIServiceServicer_to_server(server.AIServicer(populated_config), srv)
port = srv.add_insecure_port('127.0.0.1:0') port = srv.add_insecure_port('127.0.0.1:0')
srv.start() srv.start()
@@ -143,6 +144,10 @@ class TestGRPCIntegration:
def config_stub(self, channel): def config_stub(self, channel):
return stubs.ConfigStub(channel, "localhost") return stubs.ConfigStub(channel, "localhost")
@pytest.fixture
def ai_stub(self, channel):
return stubs.AIStub(channel, "localhost")
def test_list_nodes_integration(self, node_stub): def test_list_nodes_integration(self, node_stub):
nodes = node_stub.list_nodes() nodes = node_stub.list_nodes()
assert "router1" in nodes assert "router1" in nodes
@@ -170,6 +175,12 @@ class TestGRPCIntegration:
settings = config_stub.get_settings() settings = config_stub.get_settings()
assert settings["idletime"] == 99 assert settings["idletime"] == 99
def test_list_mcp_servers_integration(self, ai_stub):
ai_stub.configure_mcp("test-mcp", url="http://localhost:8080", enabled=True)
servers = ai_stub.list_mcp_servers()
assert "test-mcp" in servers
assert servers["test-mcp"]["url"] == "http://localhost:8080"
def test_add_delete_node_integration(self, node_stub): def test_add_delete_node_integration(self, node_stub):
node_stub.add_node("integration-test-node", {"host": "9.9.9.9"}) node_stub.add_node("integration-test-node", {"host": "9.9.9.9"})
assert "integration-test-node" in node_stub.list_nodes() assert "integration-test-node" in node_stub.list_nodes()
+32
View File
@@ -0,0 +1,32 @@
import pytest
from connpy.utils import log_cleaner
def test_log_cleaner_empty():
assert log_cleaner("") == ""
assert log_cleaner(None) == ""
def test_log_cleaner_plain_text():
assert log_cleaner("hello world") == "hello world"
def test_log_cleaner_ansi_colors():
# \x1b[31m is red, \x1b[0m is reset
assert log_cleaner("\x1b[31mhello\x1b[0m world") == "hello world"
def test_log_cleaner_osc_window_title():
# Set window title OSC: \x1b]0;my title\x07 followed by prompt
sample = "\x1b]0;fluzzi32@norman: ~\x07fluzzi32@norman:~$"
assert log_cleaner(sample) == "fluzzi32@norman:~$"
def test_log_cleaner_osc_with_st_terminator():
# OSC can also be terminated by \x1b\\ (ST)
sample = "\x1b]0;some title\x1b\\my_prompt>"
assert log_cleaner(sample) == "my_prompt>"
def test_log_cleaner_mixed_ansi_and_osc():
sample = "\x1b]0;title\x07\x1b[32muser@host\x1b[0m:\x1b[34m/path\x1b[0m$ "
assert log_cleaner(sample) == "user@host:/path$"
def test_log_cleaner_carriage_return_and_backspace():
# Test that standard control sequences like \r and \b still work as expected
assert log_cleaner("hello\rworld") == "world"
assert log_cleaner("hell\bo") == "helo"
+4 -1
View File
@@ -7,11 +7,14 @@ def log_cleaner(data: str) -> str:
if not data: if not data:
return "" return ""
# Remove OSC (Operating System Command) sequences (e.g., set window title \x1b]0;...\x07)
data = re.sub(r'\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)', '', data)
lines = data.split('\n') lines = data.split('\n')
cleaned_lines = [] cleaned_lines = []
# Regex to capture: ANSI sequences, control characters (\r, \b, etc), and plain text chunks # Regex to capture: ANSI sequences, control characters (\r, \b, etc), and plain text chunks
token_re = re.compile(r'(\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/ ]*[@-~])|\r|\b|\x7f|[\x00-\x1F]|[^\x1B\r\b\x7f\x00-\x1F]+)') token_re = re.compile(r'(\x1B(?:[\x30-\x5A\x5C-\x7E]|\[[0-?]*[ -/ ]*[@-~])|\r|\b|\x7f|[\x00-\x1F]|[^\x1B\r\b\x7f\x00-\x1F]+)')
for line in lines: for line in lines:
buffer = [] buffer = []
+91 -33
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.cli.ai_handler API documentation</title> <title>connpy.cli.ai_handler API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -61,13 +61,22 @@ el.replaceWith(d);
def dispatch(self, args): def dispatch(self, args):
if args.list_sessions: if args.list_sessions:
sessions = self.app.services.ai.list_sessions() limit = 20 if not getattr(args, &#34;all&#34;, False) else None
sessions, total = self.app.services.ai.list_sessions(limit=limit)
if not sessions: if not sessions:
printer.info(&#34;No saved AI sessions found.&#34;) printer.info(&#34;No saved AI sessions found.&#34;)
return return
columns = [&#34;ID&#34;, &#34;Title&#34;, &#34;Created At&#34;, &#34;Model&#34;] columns = [&#34;ID&#34;, &#34;Title&#34;, &#34;Created At&#34;, &#34;Model&#34;]
rows = [[s[&#34;id&#34;], s[&#34;title&#34;], s[&#34;created_at&#34;], s[&#34;model&#34;]] for s in sessions] rows = [[s[&#34;id&#34;], s[&#34;title&#34;], s[&#34;created_at&#34;], s[&#34;model&#34;]] for s in sessions]
printer.table(&#34;AI Persisted Sessions&#34;, columns, rows)
title = &#34;AI Persisted Sessions&#34;
if limit and total &gt; limit:
title += f&#34; (Showing last {limit} of {total})&#34;
printer.table(title, columns, rows)
if limit and total &gt; limit:
printer.info(f&#34;Use &#39;--list --all&#39; to see all {total} sessions.&#34;)
return return
if args.delete_session: if args.delete_session:
@@ -84,7 +93,7 @@ el.replaceWith(d);
# Determinar session_id para retomar # Determinar session_id para retomar
session_id = None session_id = None
if args.resume: if args.resume:
sessions = self.app.services.ai.list_sessions() sessions, _ = self.app.services.ai.list_sessions()
session_id = sessions[0][&#34;id&#34;] if sessions else None session_id = sessions[0][&#34;id&#34;] if sessions else None
if not session_id: if not session_id:
printer.warning(&#34;No previous session found to resume.&#34;) printer.warning(&#34;No previous session found to resume.&#34;)
@@ -103,15 +112,22 @@ el.replaceWith(d);
elif settings.get(key): elif settings.get(key):
arguments[key] = settings.get(key) arguments[key] = settings.get(key)
for key in [&#34;engineer_auth&#34;, &#34;architect_auth&#34;]:
cli_val = getattr(args, key, None)
if cli_val:
arguments[key] = self._parse_auth_value(cli_val[0])
elif settings.get(key):
arguments[key] = settings.get(key)
# Check keys only if running in local mode (not remote) # Check keys only if running in local mode (not remote)
if getattr(self.app.services, &#34;mode&#34;, &#34;local&#34;) == &#34;local&#34;: if getattr(self.app.services, &#34;mode&#34;, &#34;local&#34;) == &#34;local&#34;:
if not arguments.get(&#34;engineer_api_key&#34;): if not arguments.get(&#34;engineer_api_key&#34;) and not arguments.get(&#34;engineer_auth&#34;):
printer.error(&#34;Engineer API key not configured. The chat cannot start.&#34;) printer.error(&#34;Engineer API key/auth not configured. The chat cannot start.&#34;)
printer.info(&#34;Use &#39;connpy config --engineer-api-key &lt;key&gt;&#39; to set it.&#34;) printer.info(&#34;Use &#39;connpy config --engineer-api-key &lt;key&gt;&#39; or &#39;connpy config --engineer-auth &lt;auth&gt;&#39; to set it.&#34;)
sys.exit(1) sys.exit(1)
if not arguments.get(&#34;architect_api_key&#34;): if not arguments.get(&#34;architect_api_key&#34;) and not arguments.get(&#34;architect_auth&#34;):
printer.warning(&#34;Architect API key not configured. Architect will be unavailable.&#34;) printer.warning(&#34;Architect API key/auth not configured. Architect will be unavailable.&#34;)
printer.info(&#34;Use &#39;connpy config --architect-api-key &lt;key&gt;&#39; to enable it.&#34;) printer.info(&#34;Use &#39;connpy config --architect-api-key &lt;key&gt;&#39; or &#39;connpy config --architect-auth &lt;auth&gt;&#39; to enable it.&#34;)
# El resto de la interacción el CLI la maneja con el agente subyacente # El resto de la interacción el CLI la maneja con el agente subyacente
self.app.myai = self.app.services.ai self.app.myai = self.app.services.ai
@@ -148,7 +164,7 @@ el.replaceWith(d);
if history: if history:
mdprint(f&#34;[debug]Analyzing {len(history)} previous messages...[/debug]\n&#34;) mdprint(f&#34;[debug]Analyzing {len(history)} previous messages...[/debug]\n&#34;)
else: else:
printer.error(f&#34;Could not load session {session_id}. Starting clean.&#34;) printer.info(f&#34;Session &#39;{session_id}&#39; not found. Starting clean.&#34;)
if not history: if not history:
mdprint(Rule(style=&#34;engineer&#34;)) mdprint(Rule(style=&#34;engineer&#34;))
@@ -162,7 +178,7 @@ el.replaceWith(d);
if user_query.lower() in [&#39;exit&#39;, &#39;quit&#39;, &#39;bye&#39;, &#39;cancel&#39;]: break if user_query.lower() in [&#39;exit&#39;, &#39;quit&#39;, &#39;bye&#39;, &#39;cancel&#39;]: break
with console.status(&#34;[ai_status]Agent is thinking...&#34;) as status: with console.status(&#34;[ai_status]Agent is thinking...&#34;) as status:
result = self.app.myai.ask(user_query, chat_history=history, status=status, debug=args.debug, trust=args.trust, **self.ai_overrides) result = self.app.myai.ask(user_query, chat_history=history, status=status, debug=args.debug, trust=args.trust, session_id=session_id, **self.ai_overrides)
new_history = result.get(&#34;chat_history&#34;) new_history = result.get(&#34;chat_history&#34;)
if new_history is not None: if new_history is not None:
@@ -193,8 +209,7 @@ el.replaceWith(d);
action = mcp_args[0].lower() action = mcp_args[0].lower()
if action == &#34;list&#34;: if action == &#34;list&#34;:
settings = self.app.services.config_svc.get_settings() mcp_servers = self.app.services.ai.list_mcp_servers()
mcp_servers = settings.get(&#34;ai&#34;, {}).get(&#34;mcp_servers&#34;, {})
if not mcp_servers: if not mcp_servers:
printer.info(&#34;No MCP servers configured.&#34;) printer.info(&#34;No MCP servers configured.&#34;)
else: else:
@@ -259,8 +274,7 @@ el.replaceWith(d);
from .forms import Forms from .forms import Forms
self.app.cli_forms = Forms(self.app) self.app.cli_forms = Forms(self.app)
settings = self.app.services.config_svc.get_settings() mcp_servers = self.app.services.ai.list_mcp_servers()
mcp_servers = settings.get(&#34;ai&#34;, {}).get(&#34;mcp_servers&#34;, {})
result = self.app.cli_forms.mcp_wizard(mcp_servers) result = self.app.cli_forms.mcp_wizard(mcp_servers)
if not result: if not result:
@@ -294,7 +308,37 @@ el.replaceWith(d);
printer.success(f&#34;MCP server &#39;{result[&#39;name&#39;]}&#39; removed.&#34;) printer.success(f&#34;MCP server &#39;{result[&#39;name&#39;]}&#39; removed.&#34;)
except Exception as e: except Exception as e:
printer.error(str(e))</code></pre> printer.error(str(e))
def _parse_auth_value(self, value):
if not value or value.lower() in [&#34;none&#34;, &#34;clear&#34;]:
return None
import os
import yaml
import json
if os.path.exists(value):
try:
with open(value, &#34;r&#34;) as f:
content = f.read()
try:
return json.loads(content)
except ValueError:
return yaml.safe_load(content)
except Exception as e:
printer.error(f&#34;Failed to read/parse auth file &#39;{value}&#39;: {e}&#34;)
sys.exit(1)
try:
return json.loads(value)
except ValueError:
try:
parsed = yaml.safe_load(value)
if isinstance(parsed, dict):
return parsed
raise ValueError()
except Exception:
printer.error(&#34;Auth parameter must be a valid JSON/YAML string, or a path to a JSON/YAML file.&#34;)
sys.exit(1)</code></pre>
</details> </details>
<div class="desc"></div> <div class="desc"></div>
<h3>Methods</h3> <h3>Methods</h3>
@@ -316,8 +360,7 @@ el.replaceWith(d);
action = mcp_args[0].lower() action = mcp_args[0].lower()
if action == &#34;list&#34;: if action == &#34;list&#34;:
settings = self.app.services.config_svc.get_settings() mcp_servers = self.app.services.ai.list_mcp_servers()
mcp_servers = settings.get(&#34;ai&#34;, {}).get(&#34;mcp_servers&#34;, {})
if not mcp_servers: if not mcp_servers:
printer.info(&#34;No MCP servers configured.&#34;) printer.info(&#34;No MCP servers configured.&#34;)
else: else:
@@ -382,8 +425,7 @@ el.replaceWith(d);
from .forms import Forms from .forms import Forms
self.app.cli_forms = Forms(self.app) self.app.cli_forms = Forms(self.app)
settings = self.app.services.config_svc.get_settings() mcp_servers = self.app.services.ai.list_mcp_servers()
mcp_servers = settings.get(&#34;ai&#34;, {}).get(&#34;mcp_servers&#34;, {})
result = self.app.cli_forms.mcp_wizard(mcp_servers) result = self.app.cli_forms.mcp_wizard(mcp_servers)
if not result: if not result:
@@ -431,13 +473,22 @@ el.replaceWith(d);
</summary> </summary>
<pre><code class="python">def dispatch(self, args): <pre><code class="python">def dispatch(self, args):
if args.list_sessions: if args.list_sessions:
sessions = self.app.services.ai.list_sessions() limit = 20 if not getattr(args, &#34;all&#34;, False) else None
sessions, total = self.app.services.ai.list_sessions(limit=limit)
if not sessions: if not sessions:
printer.info(&#34;No saved AI sessions found.&#34;) printer.info(&#34;No saved AI sessions found.&#34;)
return return
columns = [&#34;ID&#34;, &#34;Title&#34;, &#34;Created At&#34;, &#34;Model&#34;] columns = [&#34;ID&#34;, &#34;Title&#34;, &#34;Created At&#34;, &#34;Model&#34;]
rows = [[s[&#34;id&#34;], s[&#34;title&#34;], s[&#34;created_at&#34;], s[&#34;model&#34;]] for s in sessions] rows = [[s[&#34;id&#34;], s[&#34;title&#34;], s[&#34;created_at&#34;], s[&#34;model&#34;]] for s in sessions]
printer.table(&#34;AI Persisted Sessions&#34;, columns, rows)
title = &#34;AI Persisted Sessions&#34;
if limit and total &gt; limit:
title += f&#34; (Showing last {limit} of {total})&#34;
printer.table(title, columns, rows)
if limit and total &gt; limit:
printer.info(f&#34;Use &#39;--list --all&#39; to see all {total} sessions.&#34;)
return return
if args.delete_session: if args.delete_session:
@@ -454,7 +505,7 @@ el.replaceWith(d);
# Determinar session_id para retomar # Determinar session_id para retomar
session_id = None session_id = None
if args.resume: if args.resume:
sessions = self.app.services.ai.list_sessions() sessions, _ = self.app.services.ai.list_sessions()
session_id = sessions[0][&#34;id&#34;] if sessions else None session_id = sessions[0][&#34;id&#34;] if sessions else None
if not session_id: if not session_id:
printer.warning(&#34;No previous session found to resume.&#34;) printer.warning(&#34;No previous session found to resume.&#34;)
@@ -473,15 +524,22 @@ el.replaceWith(d);
elif settings.get(key): elif settings.get(key):
arguments[key] = settings.get(key) arguments[key] = settings.get(key)
for key in [&#34;engineer_auth&#34;, &#34;architect_auth&#34;]:
cli_val = getattr(args, key, None)
if cli_val:
arguments[key] = self._parse_auth_value(cli_val[0])
elif settings.get(key):
arguments[key] = settings.get(key)
# Check keys only if running in local mode (not remote) # Check keys only if running in local mode (not remote)
if getattr(self.app.services, &#34;mode&#34;, &#34;local&#34;) == &#34;local&#34;: if getattr(self.app.services, &#34;mode&#34;, &#34;local&#34;) == &#34;local&#34;:
if not arguments.get(&#34;engineer_api_key&#34;): if not arguments.get(&#34;engineer_api_key&#34;) and not arguments.get(&#34;engineer_auth&#34;):
printer.error(&#34;Engineer API key not configured. The chat cannot start.&#34;) printer.error(&#34;Engineer API key/auth not configured. The chat cannot start.&#34;)
printer.info(&#34;Use &#39;connpy config --engineer-api-key &lt;key&gt;&#39; to set it.&#34;) printer.info(&#34;Use &#39;connpy config --engineer-api-key &lt;key&gt;&#39; or &#39;connpy config --engineer-auth &lt;auth&gt;&#39; to set it.&#34;)
sys.exit(1) sys.exit(1)
if not arguments.get(&#34;architect_api_key&#34;): if not arguments.get(&#34;architect_api_key&#34;) and not arguments.get(&#34;architect_auth&#34;):
printer.warning(&#34;Architect API key not configured. Architect will be unavailable.&#34;) printer.warning(&#34;Architect API key/auth not configured. Architect will be unavailable.&#34;)
printer.info(&#34;Use &#39;connpy config --architect-api-key &lt;key&gt;&#39; to enable it.&#34;) printer.info(&#34;Use &#39;connpy config --architect-api-key &lt;key&gt;&#39; or &#39;connpy config --architect-auth &lt;auth&gt;&#39; to enable it.&#34;)
# El resto de la interacción el CLI la maneja con el agente subyacente # El resto de la interacción el CLI la maneja con el agente subyacente
self.app.myai = self.app.services.ai self.app.myai = self.app.services.ai
@@ -512,7 +570,7 @@ el.replaceWith(d);
if history: if history:
mdprint(f&#34;[debug]Analyzing {len(history)} previous messages...[/debug]\n&#34;) mdprint(f&#34;[debug]Analyzing {len(history)} previous messages...[/debug]\n&#34;)
else: else:
printer.error(f&#34;Could not load session {session_id}. Starting clean.&#34;) printer.info(f&#34;Session &#39;{session_id}&#39; not found. Starting clean.&#34;)
if not history: if not history:
mdprint(Rule(style=&#34;engineer&#34;)) mdprint(Rule(style=&#34;engineer&#34;))
@@ -526,7 +584,7 @@ el.replaceWith(d);
if user_query.lower() in [&#39;exit&#39;, &#39;quit&#39;, &#39;bye&#39;, &#39;cancel&#39;]: break if user_query.lower() in [&#39;exit&#39;, &#39;quit&#39;, &#39;bye&#39;, &#39;cancel&#39;]: break
with console.status(&#34;[ai_status]Agent is thinking...&#34;) as status: with console.status(&#34;[ai_status]Agent is thinking...&#34;) as status:
result = self.app.myai.ask(user_query, chat_history=history, status=status, debug=args.debug, trust=args.trust, **self.ai_overrides) result = self.app.myai.ask(user_query, chat_history=history, status=status, debug=args.debug, trust=args.trust, session_id=session_id, **self.ai_overrides)
new_history = result.get(&#34;chat_history&#34;) new_history = result.get(&#34;chat_history&#34;)
if new_history is not None: if new_history is not None:
@@ -608,7 +666,7 @@ el.replaceWith(d);
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.cli.api_handler API documentation</title> <title>connpy.cli.api_handler API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -193,7 +193,7 @@ el.replaceWith(d);
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+76 -7
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.cli.config_handler API documentation</title> <title>connpy.cli.config_handler API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -70,8 +70,10 @@ el.replaceWith(d);
&#34;theme&#34;: self.set_theme, &#34;theme&#34;: self.set_theme,
&#34;engineer_model&#34;: self.set_ai_config, &#34;engineer_model&#34;: self.set_ai_config,
&#34;engineer_api_key&#34;: self.set_ai_config, &#34;engineer_api_key&#34;: self.set_ai_config,
&#34;engineer_auth&#34;: self.set_ai_config,
&#34;architect_model&#34;: self.set_ai_config, &#34;architect_model&#34;: self.set_ai_config,
&#34;architect_api_key&#34;: self.set_ai_config, &#34;architect_api_key&#34;: self.set_ai_config,
&#34;architect_auth&#34;: self.set_ai_config,
&#34;trusted_commands&#34;: self.set_ai_config, &#34;trusted_commands&#34;: self.set_ai_config,
&#34;service_mode&#34;: self.set_service_mode, &#34;service_mode&#34;: self.set_service_mode,
&#34;remote_host&#34;: self.set_remote_host, &#34;remote_host&#34;: self.set_remote_host,
@@ -178,11 +180,59 @@ el.replaceWith(d);
try: try:
settings = self.app.services.config_svc.get_settings() settings = self.app.services.config_svc.get_settings()
aiconfig = settings.get(&#34;ai&#34;, {}) aiconfig = settings.get(&#34;ai&#34;, {})
aiconfig[args.command] = args.data[0] val = args.data[0]
# Check for unset/clear request
if val.lower() in [&#34;none&#34;, &#34;clear&#34;, &#34;&#34;]:
if args.command in aiconfig:
del aiconfig[args.command]
else:
# If configuring auth, parse as dictionary (JSON/YAML or file path)
if args.command in [&#34;engineer_auth&#34;, &#34;architect_auth&#34;]:
parsed_val = self._parse_auth_value(val)
if parsed_val is not None:
aiconfig[args.command] = parsed_val
else:
if args.command in aiconfig:
del aiconfig[args.command]
else:
aiconfig[args.command] = val
self.app.services.config_svc.update_setting(&#34;ai&#34;, aiconfig) self.app.services.config_svc.update_setting(&#34;ai&#34;, aiconfig)
printer.success(&#34;Config saved&#34;) printer.success(&#34;Config saved&#34;)
except ConnpyError as e: except (ConnpyError, InvalidConfigurationError) as e:
printer.error(str(e))</code></pre> printer.error(str(e))
def _parse_auth_value(self, value):
if value.lower() in [&#34;none&#34;, &#34;clear&#34;, &#34;&#34;]:
return None
# Check if it&#39;s a file path
import os
if os.path.exists(value):
try:
with open(value, &#34;r&#34;) as f:
content = f.read()
import json
try:
return json.loads(content)
except ValueError:
return yaml.safe_load(content)
except Exception as e:
raise InvalidConfigurationError(f&#34;Failed to read/parse auth file &#39;{value}&#39;: {e}&#34;)
# Try parsing as inline JSON/YAML
try:
import json
return json.loads(value)
except ValueError:
try:
parsed = yaml.safe_load(value)
if isinstance(parsed, dict):
return parsed
raise ValueError()
except Exception:
raise InvalidConfigurationError(&#34;Auth parameter must be a valid JSON/YAML string, or a path to a JSON/YAML file.&#34;)</code></pre>
</details> </details>
<div class="desc"></div> <div class="desc"></div>
<h3>Methods</h3> <h3>Methods</h3>
@@ -206,8 +256,10 @@ el.replaceWith(d);
&#34;theme&#34;: self.set_theme, &#34;theme&#34;: self.set_theme,
&#34;engineer_model&#34;: self.set_ai_config, &#34;engineer_model&#34;: self.set_ai_config,
&#34;engineer_api_key&#34;: self.set_ai_config, &#34;engineer_api_key&#34;: self.set_ai_config,
&#34;engineer_auth&#34;: self.set_ai_config,
&#34;architect_model&#34;: self.set_ai_config, &#34;architect_model&#34;: self.set_ai_config,
&#34;architect_api_key&#34;: self.set_ai_config, &#34;architect_api_key&#34;: self.set_ai_config,
&#34;architect_auth&#34;: self.set_ai_config,
&#34;trusted_commands&#34;: self.set_ai_config, &#34;trusted_commands&#34;: self.set_ai_config,
&#34;service_mode&#34;: self.set_service_mode, &#34;service_mode&#34;: self.set_service_mode,
&#34;remote_host&#34;: self.set_remote_host, &#34;remote_host&#34;: self.set_remote_host,
@@ -234,10 +286,27 @@ el.replaceWith(d);
try: try:
settings = self.app.services.config_svc.get_settings() settings = self.app.services.config_svc.get_settings()
aiconfig = settings.get(&#34;ai&#34;, {}) aiconfig = settings.get(&#34;ai&#34;, {})
aiconfig[args.command] = args.data[0] val = args.data[0]
# Check for unset/clear request
if val.lower() in [&#34;none&#34;, &#34;clear&#34;, &#34;&#34;]:
if args.command in aiconfig:
del aiconfig[args.command]
else:
# If configuring auth, parse as dictionary (JSON/YAML or file path)
if args.command in [&#34;engineer_auth&#34;, &#34;architect_auth&#34;]:
parsed_val = self._parse_auth_value(val)
if parsed_val is not None:
aiconfig[args.command] = parsed_val
else:
if args.command in aiconfig:
del aiconfig[args.command]
else:
aiconfig[args.command] = val
self.app.services.config_svc.update_setting(&#34;ai&#34;, aiconfig) self.app.services.config_svc.update_setting(&#34;ai&#34;, aiconfig)
printer.success(&#34;Config saved&#34;) printer.success(&#34;Config saved&#34;)
except ConnpyError as e: except (ConnpyError, InvalidConfigurationError) as e:
printer.error(str(e))</code></pre> printer.error(str(e))</code></pre>
</details> </details>
<div class="desc"></div> <div class="desc"></div>
@@ -482,7 +551,7 @@ el.replaceWith(d);
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.cli.context_handler API documentation</title> <title>connpy.cli.context_handler API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -249,7 +249,7 @@ el.replaceWith(d);
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.cli.forms API documentation</title> <title>connpy.cli.forms API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -690,7 +690,7 @@ el.replaceWith(d);
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.cli.help_text API documentation</title> <title>connpy.cli.help_text API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -303,7 +303,7 @@ tasks:
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+129 -3
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.cli.helpers API documentation</title> <title>connpy.cli.helpers API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -69,7 +69,7 @@ el.replaceWith(d);
return answer[0] return answer[0]
else: else:
questions = [inquirer.List(name, message=&#34;Pick {} to {}:&#34;.format(name,action), choices=list_, carousel=True)] questions = [inquirer.List(name, message=&#34;Pick {} to {}:&#34;.format(name,action), choices=list_, carousel=True)]
answer = inquirer.prompt(questions) answer = inquirer.prompt(questions, theme=theme)
if answer == None: if answer == None:
return None return None
else: else:
@@ -115,6 +115,65 @@ el.replaceWith(d);
</details> </details>
<div class="desc"></div> <div class="desc"></div>
</dd> </dd>
<dt id="connpy.cli.helpers.get_theme"><code class="name flex">
<span>def <span class="ident">get_theme</span></span>(<span>)</span>
</code></dt>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">def get_theme():
&#34;&#34;&#34;Returns a fresh instance of the theme with current colors.&#34;&#34;&#34;
return ConnpyTheme()</code></pre>
</details>
<div class="desc"><p>Returns a fresh instance of the theme with current colors.</p></div>
</dd>
<dt id="connpy.cli.helpers.hex_to_blessed"><code class="name flex">
<span>def <span class="ident">hex_to_blessed</span></span>(<span>hex_str)</span>
</code></dt>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">def hex_to_blessed(hex_str):
&#34;&#34;&#34;Convert hex color string to blessed/ansi format.&#34;&#34;&#34;
if not hex_str or not isinstance(hex_str, str):
return term.normal
# Check for bold prefix
prefix = &#34;&#34;
if hex_str.startswith(&#39;bold &#39;):
prefix = term.bold
hex_str = hex_str.replace(&#39;bold &#39;, &#39;&#39;).strip()
# If it&#39;s a standard color name
if not hex_str.startswith(&#39;#&#39;):
return prefix + getattr(term, hex_str, term.normal)
# Parse hex
try:
h = hex_str.lstrip(&#39;#&#39;)
if len(h) == 3:
h = &#39;&#39;.join([c*2 for c in h])
r = int(h[0:2], 16)
g = int(h[2:4], 16)
b = int(h[4:6], 16)
# Try RGB, fallback to standard cyan if it fails or returns empty
try:
c = term.color_rgb(r, g, b)
if not c: # Some terms return empty for RGB
return prefix + term.cyan
return prefix + c
except:
return prefix + term.cyan
except:
return prefix + term.normal</code></pre>
</details>
<div class="desc"><p>Convert hex color string to blessed/ansi format.</p></div>
</dd>
<dt id="connpy.cli.helpers.nodes_completer"><code class="name flex"> <dt id="connpy.cli.helpers.nodes_completer"><code class="name flex">
<span>def <span class="ident">nodes_completer</span></span>(<span>prefix, parsed_args, **kwargs)</span> <span>def <span class="ident">nodes_completer</span></span>(<span>prefix, parsed_args, **kwargs)</span>
</code></dt> </code></dt>
@@ -181,6 +240,61 @@ el.replaceWith(d);
</dl> </dl>
</section> </section>
<section> <section>
<h2 class="section-title" id="header-classes">Classes</h2>
<dl>
<dt id="connpy.cli.helpers.ConnpyTheme"><code class="flex name class">
<span>class <span class="ident">ConnpyTheme</span></span>
</code></dt>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">class ConnpyTheme(Default):
def __init__(self):
super().__init__()
try:
from ..printer import _global_active_styles
# Use user_prompt as primary accent, fallback to info/cyan
accent = _global_active_styles.get(&#34;user_prompt&#34;, _global_active_styles.get(&#34;info&#34;, &#34;cyan&#34;))
accent_color = hex_to_blessed(accent)
self.Question.mark_color = accent_color
self.List.selection_color = accent_color
self.List.selection_cursor = &#34;&gt;&#34;
except:
# Absolute fallback to standard cyan
self.Question.mark_color = term.cyan
self.List.selection_color = term.bold_cyan
self.List.selection_cursor = &#34;&gt;&#34;</code></pre>
</details>
<div class="desc"></div>
<h3>Ancestors</h3>
<ul class="hlist">
<li>inquirer.themes.Default</li>
<li>inquirer.themes.Theme</li>
</ul>
</dd>
<dt id="connpy.cli.helpers.ThemeProxy"><code class="flex name class">
<span>class <span class="ident">ThemeProxy</span></span>
</code></dt>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">class ThemeProxy:
&#34;&#34;&#34;Proxy to ensure theme colors are resolved at runtime.&#34;&#34;&#34;
def __getattr__(self, name):
return getattr(get_theme(), name)
def __iter__(self):
return iter(get_theme())
def __getitem__(self, item):
return get_theme()[item]</code></pre>
</details>
<div class="desc"><p>Proxy to ensure theme colors are resolved at runtime.</p></div>
</dd>
</dl>
</section> </section>
</article> </article>
<nav id="sidebar"> <nav id="sidebar">
@@ -198,16 +312,28 @@ el.replaceWith(d);
<li><code><a title="connpy.cli.helpers.choose" href="#connpy.cli.helpers.choose">choose</a></code></li> <li><code><a title="connpy.cli.helpers.choose" href="#connpy.cli.helpers.choose">choose</a></code></li>
<li><code><a title="connpy.cli.helpers.folders_completer" href="#connpy.cli.helpers.folders_completer">folders_completer</a></code></li> <li><code><a title="connpy.cli.helpers.folders_completer" href="#connpy.cli.helpers.folders_completer">folders_completer</a></code></li>
<li><code><a title="connpy.cli.helpers.get_config_dir" href="#connpy.cli.helpers.get_config_dir">get_config_dir</a></code></li> <li><code><a title="connpy.cli.helpers.get_config_dir" href="#connpy.cli.helpers.get_config_dir">get_config_dir</a></code></li>
<li><code><a title="connpy.cli.helpers.get_theme" href="#connpy.cli.helpers.get_theme">get_theme</a></code></li>
<li><code><a title="connpy.cli.helpers.hex_to_blessed" href="#connpy.cli.helpers.hex_to_blessed">hex_to_blessed</a></code></li>
<li><code><a title="connpy.cli.helpers.nodes_completer" href="#connpy.cli.helpers.nodes_completer">nodes_completer</a></code></li> <li><code><a title="connpy.cli.helpers.nodes_completer" href="#connpy.cli.helpers.nodes_completer">nodes_completer</a></code></li>
<li><code><a title="connpy.cli.helpers.profiles_completer" href="#connpy.cli.helpers.profiles_completer">profiles_completer</a></code></li> <li><code><a title="connpy.cli.helpers.profiles_completer" href="#connpy.cli.helpers.profiles_completer">profiles_completer</a></code></li>
<li><code><a title="connpy.cli.helpers.toplevel_completer" href="#connpy.cli.helpers.toplevel_completer">toplevel_completer</a></code></li> <li><code><a title="connpy.cli.helpers.toplevel_completer" href="#connpy.cli.helpers.toplevel_completer">toplevel_completer</a></code></li>
</ul> </ul>
</li> </li>
<li><h3><a href="#header-classes">Classes</a></h3>
<ul>
<li>
<h4><code><a title="connpy.cli.helpers.ConnpyTheme" href="#connpy.cli.helpers.ConnpyTheme">ConnpyTheme</a></code></h4>
</li>
<li>
<h4><code><a title="connpy.cli.helpers.ThemeProxy" href="#connpy.cli.helpers.ThemeProxy">ThemeProxy</a></code></h4>
</li>
</ul>
</li>
</ul> </ul>
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.cli.import_export_handler API documentation</title> <title>connpy.cli.import_export_handler API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -272,7 +272,7 @@ el.replaceWith(d);
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.cli API documentation</title> <title>connpy.cli API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -142,7 +142,7 @@ el.replaceWith(d);
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.cli.node_handler API documentation</title> <title>connpy.cli.node_handler API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -606,7 +606,7 @@ el.replaceWith(d);
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.cli.plugin_handler API documentation</title> <title>connpy.cli.plugin_handler API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -385,7 +385,7 @@ el.replaceWith(d);
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.cli.profile_handler API documentation</title> <title>connpy.cli.profile_handler API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -314,7 +314,7 @@ el.replaceWith(d);
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.cli.run_handler API documentation</title> <title>connpy.cli.run_handler API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -454,7 +454,7 @@ el.replaceWith(d);
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.cli.sync_handler API documentation</title> <title>connpy.cli.sync_handler API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -427,7 +427,7 @@ el.replaceWith(d);
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+8 -8
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.cli.terminal_ui API documentation</title> <title>connpy.cli.terminal_ui API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -168,7 +168,8 @@ el.replaceWith(d);
if state[&#39;context_mode&#39;] == self.mode_single: if state[&#39;context_mode&#39;] == self.mode_single:
active_raw = raw_bytes[start:end] active_raw = raw_bytes[start:end]
else: else:
active_raw = raw_bytes[start:] # Concat only the bytes of valid blocks to skip intermediate empty/cancelled prompt noise
active_raw = b&#34;&#34;.join(raw_bytes[b[0]:b[1]] for b in blocks[idx:])
return preview + &#34;\n&#34; + log_cleaner(active_raw.decode(errors=&#39;replace&#39;)) return preview + &#34;\n&#34; + log_cleaner(active_raw.decode(errors=&#39;replace&#39;))
def get_prompt_text(): def get_prompt_text():
@@ -207,7 +208,6 @@ el.replaceWith(d);
base_str = f&#39;\u25b6 Ctrl+\u2191/\u2193 adjusts by 50 lines [Tab: {m_label}]&#39; base_str = f&#39;\u25b6 Ctrl+\u2191/\u2193 adjusts by 50 lines [Tab: {m_label}]&#39;
else: else:
idx = max(0, state[&#39;total_cmds&#39;] - state[&#39;context_cmd&#39;]) idx = max(0, state[&#39;total_cmds&#39;] - state[&#39;context_cmd&#39;])
import re
def clean_preview(text): def clean_preview(text):
# Limpia saltos de línea y el prompt inicial (todo hasta #, &gt; o $) para que quede solo el comando # Limpia saltos de línea y el prompt inicial (todo hasta #, &gt; o $) para que quede solo el comando
@@ -370,10 +370,10 @@ el.replaceWith(d);
persona_title = &#34;Network Architect&#34; if active_persona == &#34;architect&#34; else &#34;Network Engineer&#34; persona_title = &#34;Network Architect&#34; if active_persona == &#34;architect&#34; else &#34;Network Engineer&#34;
active_buffer = get_active_buffer() active_buffer = get_active_buffer()
live_text = &#34;&#34; live_text = &#34;&#34;
first_chunk = True first_chunk = True
import sys
from rich.rule import Rule from rich.rule import Rule
from rich.status import Status from rich.status import Status
from connpy.printer import IncrementalMarkdownParser from connpy.printer import IncrementalMarkdownParser
@@ -622,7 +622,8 @@ el.replaceWith(d);
if state[&#39;context_mode&#39;] == self.mode_single: if state[&#39;context_mode&#39;] == self.mode_single:
active_raw = raw_bytes[start:end] active_raw = raw_bytes[start:end]
else: else:
active_raw = raw_bytes[start:] # Concat only the bytes of valid blocks to skip intermediate empty/cancelled prompt noise
active_raw = b&#34;&#34;.join(raw_bytes[b[0]:b[1]] for b in blocks[idx:])
return preview + &#34;\n&#34; + log_cleaner(active_raw.decode(errors=&#39;replace&#39;)) return preview + &#34;\n&#34; + log_cleaner(active_raw.decode(errors=&#39;replace&#39;))
def get_prompt_text(): def get_prompt_text():
@@ -661,7 +662,6 @@ el.replaceWith(d);
base_str = f&#39;\u25b6 Ctrl+\u2191/\u2193 adjusts by 50 lines [Tab: {m_label}]&#39; base_str = f&#39;\u25b6 Ctrl+\u2191/\u2193 adjusts by 50 lines [Tab: {m_label}]&#39;
else: else:
idx = max(0, state[&#39;total_cmds&#39;] - state[&#39;context_cmd&#39;]) idx = max(0, state[&#39;total_cmds&#39;] - state[&#39;context_cmd&#39;])
import re
def clean_preview(text): def clean_preview(text):
# Limpia saltos de línea y el prompt inicial (todo hasta #, &gt; o $) para que quede solo el comando # Limpia saltos de línea y el prompt inicial (todo hasta #, &gt; o $) para que quede solo el comando
@@ -824,10 +824,10 @@ el.replaceWith(d);
persona_title = &#34;Network Architect&#34; if active_persona == &#34;architect&#34; else &#34;Network Engineer&#34; persona_title = &#34;Network Architect&#34; if active_persona == &#34;architect&#34; else &#34;Network Engineer&#34;
active_buffer = get_active_buffer() active_buffer = get_active_buffer()
live_text = &#34;&#34; live_text = &#34;&#34;
first_chunk = True first_chunk = True
import sys
from rich.rule import Rule from rich.rule import Rule
from rich.status import Status from rich.status import Status
from connpy.printer import IncrementalMarkdownParser from connpy.printer import IncrementalMarkdownParser
@@ -1017,7 +1017,7 @@ on_ai_call: async function(active_buffer, question) -&gt; result_dict</p></div>
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.cli.validators API documentation</title> <title>connpy.cli.validators API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -508,7 +508,7 @@ el.replaceWith(d);
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+2 -809
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.grpc_layer.connpy_pb2 API documentation</title> <title>connpy.grpc_layer.connpy_pb2 API documentation</title>
<meta name="description" content="Generated protocol buffer code."> <meta name="description" content="Generated protocol buffer code.">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -45,617 +45,6 @@ el.replaceWith(d);
<section> <section>
</section> </section>
<section> <section>
<h2 class="section-title" id="header-classes">Classes</h2>
<dl>
<dt id="connpy.grpc_layer.connpy_pb2.AIResponse"><code class="flex name class">
<span>class <span class="ident">AIResponse</span></span>
<span>(</span><span>*args, **kwargs)</span>
</code></dt>
<dd>
<div class="desc"><p>A ProtocolMessage</p></div>
<h3>Ancestors</h3>
<ul class="hlist">
<li>google._upb._message.Message</li>
<li>google.protobuf.message.Message</li>
</ul>
<h3>Class variables</h3>
<dl>
<dt id="connpy.grpc_layer.connpy_pb2.AIResponse.DESCRIPTOR"><code class="name">var <span class="ident">DESCRIPTOR</span></code></dt>
<dd>
<div class="desc"></div>
</dd>
</dl>
</dd>
<dt id="connpy.grpc_layer.connpy_pb2.AskRequest"><code class="flex name class">
<span>class <span class="ident">AskRequest</span></span>
<span>(</span><span>*args, **kwargs)</span>
</code></dt>
<dd>
<div class="desc"><p>A ProtocolMessage</p></div>
<h3>Ancestors</h3>
<ul class="hlist">
<li>google._upb._message.Message</li>
<li>google.protobuf.message.Message</li>
</ul>
<h3>Class variables</h3>
<dl>
<dt id="connpy.grpc_layer.connpy_pb2.AskRequest.DESCRIPTOR"><code class="name">var <span class="ident">DESCRIPTOR</span></code></dt>
<dd>
<div class="desc"></div>
</dd>
</dl>
</dd>
<dt id="connpy.grpc_layer.connpy_pb2.BoolResponse"><code class="flex name class">
<span>class <span class="ident">BoolResponse</span></span>
<span>(</span><span>*args, **kwargs)</span>
</code></dt>
<dd>
<div class="desc"><p>A ProtocolMessage</p></div>
<h3>Ancestors</h3>
<ul class="hlist">
<li>google._upb._message.Message</li>
<li>google.protobuf.message.Message</li>
</ul>
<h3>Class variables</h3>
<dl>
<dt id="connpy.grpc_layer.connpy_pb2.BoolResponse.DESCRIPTOR"><code class="name">var <span class="ident">DESCRIPTOR</span></code></dt>
<dd>
<div class="desc"></div>
</dd>
</dl>
</dd>
<dt id="connpy.grpc_layer.connpy_pb2.BulkRequest"><code class="flex name class">
<span>class <span class="ident">BulkRequest</span></span>
<span>(</span><span>*args, **kwargs)</span>
</code></dt>
<dd>
<div class="desc"><p>A ProtocolMessage</p></div>
<h3>Ancestors</h3>
<ul class="hlist">
<li>google._upb._message.Message</li>
<li>google.protobuf.message.Message</li>
</ul>
<h3>Class variables</h3>
<dl>
<dt id="connpy.grpc_layer.connpy_pb2.BulkRequest.DESCRIPTOR"><code class="name">var <span class="ident">DESCRIPTOR</span></code></dt>
<dd>
<div class="desc"></div>
</dd>
</dl>
</dd>
<dt id="connpy.grpc_layer.connpy_pb2.CopilotRequest"><code class="flex name class">
<span>class <span class="ident">CopilotRequest</span></span>
<span>(</span><span>*args, **kwargs)</span>
</code></dt>
<dd>
<div class="desc"><p>A ProtocolMessage</p></div>
<h3>Ancestors</h3>
<ul class="hlist">
<li>google._upb._message.Message</li>
<li>google.protobuf.message.Message</li>
</ul>
<h3>Class variables</h3>
<dl>
<dt id="connpy.grpc_layer.connpy_pb2.CopilotRequest.DESCRIPTOR"><code class="name">var <span class="ident">DESCRIPTOR</span></code></dt>
<dd>
<div class="desc"></div>
</dd>
</dl>
</dd>
<dt id="connpy.grpc_layer.connpy_pb2.CopilotResponse"><code class="flex name class">
<span>class <span class="ident">CopilotResponse</span></span>
<span>(</span><span>*args, **kwargs)</span>
</code></dt>
<dd>
<div class="desc"><p>A ProtocolMessage</p></div>
<h3>Ancestors</h3>
<ul class="hlist">
<li>google._upb._message.Message</li>
<li>google.protobuf.message.Message</li>
</ul>
<h3>Class variables</h3>
<dl>
<dt id="connpy.grpc_layer.connpy_pb2.CopilotResponse.DESCRIPTOR"><code class="name">var <span class="ident">DESCRIPTOR</span></code></dt>
<dd>
<div class="desc"></div>
</dd>
</dl>
</dd>
<dt id="connpy.grpc_layer.connpy_pb2.DeleteRequest"><code class="flex name class">
<span>class <span class="ident">DeleteRequest</span></span>
<span>(</span><span>*args, **kwargs)</span>
</code></dt>
<dd>
<div class="desc"><p>A ProtocolMessage</p></div>
<h3>Ancestors</h3>
<ul class="hlist">
<li>google._upb._message.Message</li>
<li>google.protobuf.message.Message</li>
</ul>
<h3>Class variables</h3>
<dl>
<dt id="connpy.grpc_layer.connpy_pb2.DeleteRequest.DESCRIPTOR"><code class="name">var <span class="ident">DESCRIPTOR</span></code></dt>
<dd>
<div class="desc"></div>
</dd>
</dl>
</dd>
<dt id="connpy.grpc_layer.connpy_pb2.ExportRequest"><code class="flex name class">
<span>class <span class="ident">ExportRequest</span></span>
<span>(</span><span>*args, **kwargs)</span>
</code></dt>
<dd>
<div class="desc"><p>A ProtocolMessage</p></div>
<h3>Ancestors</h3>
<ul class="hlist">
<li>google._upb._message.Message</li>
<li>google.protobuf.message.Message</li>
</ul>
<h3>Class variables</h3>
<dl>
<dt id="connpy.grpc_layer.connpy_pb2.ExportRequest.DESCRIPTOR"><code class="name">var <span class="ident">DESCRIPTOR</span></code></dt>
<dd>
<div class="desc"></div>
</dd>
</dl>
</dd>
<dt id="connpy.grpc_layer.connpy_pb2.FilterRequest"><code class="flex name class">
<span>class <span class="ident">FilterRequest</span></span>
<span>(</span><span>*args, **kwargs)</span>
</code></dt>
<dd>
<div class="desc"><p>A ProtocolMessage</p></div>
<h3>Ancestors</h3>
<ul class="hlist">
<li>google._upb._message.Message</li>
<li>google.protobuf.message.Message</li>
</ul>
<h3>Class variables</h3>
<dl>
<dt id="connpy.grpc_layer.connpy_pb2.FilterRequest.DESCRIPTOR"><code class="name">var <span class="ident">DESCRIPTOR</span></code></dt>
<dd>
<div class="desc"></div>
</dd>
</dl>
</dd>
<dt id="connpy.grpc_layer.connpy_pb2.FullReplaceRequest"><code class="flex name class">
<span>class <span class="ident">FullReplaceRequest</span></span>
<span>(</span><span>*args, **kwargs)</span>
</code></dt>
<dd>
<div class="desc"><p>A ProtocolMessage</p></div>
<h3>Ancestors</h3>
<ul class="hlist">
<li>google._upb._message.Message</li>
<li>google.protobuf.message.Message</li>
</ul>
<h3>Class variables</h3>
<dl>
<dt id="connpy.grpc_layer.connpy_pb2.FullReplaceRequest.DESCRIPTOR"><code class="name">var <span class="ident">DESCRIPTOR</span></code></dt>
<dd>
<div class="desc"></div>
</dd>
</dl>
</dd>
<dt id="connpy.grpc_layer.connpy_pb2.IdRequest"><code class="flex name class">
<span>class <span class="ident">IdRequest</span></span>
<span>(</span><span>*args, **kwargs)</span>
</code></dt>
<dd>
<div class="desc"><p>A ProtocolMessage</p></div>
<h3>Ancestors</h3>
<ul class="hlist">
<li>google._upb._message.Message</li>
<li>google.protobuf.message.Message</li>
</ul>
<h3>Class variables</h3>
<dl>
<dt id="connpy.grpc_layer.connpy_pb2.IdRequest.DESCRIPTOR"><code class="name">var <span class="ident">DESCRIPTOR</span></code></dt>
<dd>
<div class="desc"></div>
</dd>
</dl>
</dd>
<dt id="connpy.grpc_layer.connpy_pb2.IntRequest"><code class="flex name class">
<span>class <span class="ident">IntRequest</span></span>
<span>(</span><span>*args, **kwargs)</span>
</code></dt>
<dd>
<div class="desc"><p>A ProtocolMessage</p></div>
<h3>Ancestors</h3>
<ul class="hlist">
<li>google._upb._message.Message</li>
<li>google.protobuf.message.Message</li>
</ul>
<h3>Class variables</h3>
<dl>
<dt id="connpy.grpc_layer.connpy_pb2.IntRequest.DESCRIPTOR"><code class="name">var <span class="ident">DESCRIPTOR</span></code></dt>
<dd>
<div class="desc"></div>
</dd>
</dl>
</dd>
<dt id="connpy.grpc_layer.connpy_pb2.InteractRequest"><code class="flex name class">
<span>class <span class="ident">InteractRequest</span></span>
<span>(</span><span>*args, **kwargs)</span>
</code></dt>
<dd>
<div class="desc"><p>A ProtocolMessage</p></div>
<h3>Ancestors</h3>
<ul class="hlist">
<li>google._upb._message.Message</li>
<li>google.protobuf.message.Message</li>
</ul>
<h3>Class variables</h3>
<dl>
<dt id="connpy.grpc_layer.connpy_pb2.InteractRequest.DESCRIPTOR"><code class="name">var <span class="ident">DESCRIPTOR</span></code></dt>
<dd>
<div class="desc"></div>
</dd>
</dl>
</dd>
<dt id="connpy.grpc_layer.connpy_pb2.InteractResponse"><code class="flex name class">
<span>class <span class="ident">InteractResponse</span></span>
<span>(</span><span>*args, **kwargs)</span>
</code></dt>
<dd>
<div class="desc"><p>A ProtocolMessage</p></div>
<h3>Ancestors</h3>
<ul class="hlist">
<li>google._upb._message.Message</li>
<li>google.protobuf.message.Message</li>
</ul>
<h3>Class variables</h3>
<dl>
<dt id="connpy.grpc_layer.connpy_pb2.InteractResponse.DESCRIPTOR"><code class="name">var <span class="ident">DESCRIPTOR</span></code></dt>
<dd>
<div class="desc"></div>
</dd>
</dl>
</dd>
<dt id="connpy.grpc_layer.connpy_pb2.ListRequest"><code class="flex name class">
<span>class <span class="ident">ListRequest</span></span>
<span>(</span><span>*args, **kwargs)</span>
</code></dt>
<dd>
<div class="desc"><p>A ProtocolMessage</p></div>
<h3>Ancestors</h3>
<ul class="hlist">
<li>google._upb._message.Message</li>
<li>google.protobuf.message.Message</li>
</ul>
<h3>Class variables</h3>
<dl>
<dt id="connpy.grpc_layer.connpy_pb2.ListRequest.DESCRIPTOR"><code class="name">var <span class="ident">DESCRIPTOR</span></code></dt>
<dd>
<div class="desc"></div>
</dd>
</dl>
</dd>
<dt id="connpy.grpc_layer.connpy_pb2.MCPRequest"><code class="flex name class">
<span>class <span class="ident">MCPRequest</span></span>
<span>(</span><span>*args, **kwargs)</span>
</code></dt>
<dd>
<div class="desc"><p>A ProtocolMessage</p></div>
<h3>Ancestors</h3>
<ul class="hlist">
<li>google._upb._message.Message</li>
<li>google.protobuf.message.Message</li>
</ul>
<h3>Class variables</h3>
<dl>
<dt id="connpy.grpc_layer.connpy_pb2.MCPRequest.DESCRIPTOR"><code class="name">var <span class="ident">DESCRIPTOR</span></code></dt>
<dd>
<div class="desc"></div>
</dd>
</dl>
</dd>
<dt id="connpy.grpc_layer.connpy_pb2.MessageValue"><code class="flex name class">
<span>class <span class="ident">MessageValue</span></span>
<span>(</span><span>*args, **kwargs)</span>
</code></dt>
<dd>
<div class="desc"><p>A ProtocolMessage</p></div>
<h3>Ancestors</h3>
<ul class="hlist">
<li>google._upb._message.Message</li>
<li>google.protobuf.message.Message</li>
</ul>
<h3>Class variables</h3>
<dl>
<dt id="connpy.grpc_layer.connpy_pb2.MessageValue.DESCRIPTOR"><code class="name">var <span class="ident">DESCRIPTOR</span></code></dt>
<dd>
<div class="desc"></div>
</dd>
</dl>
</dd>
<dt id="connpy.grpc_layer.connpy_pb2.MoveRequest"><code class="flex name class">
<span>class <span class="ident">MoveRequest</span></span>
<span>(</span><span>*args, **kwargs)</span>
</code></dt>
<dd>
<div class="desc"><p>A ProtocolMessage</p></div>
<h3>Ancestors</h3>
<ul class="hlist">
<li>google._upb._message.Message</li>
<li>google.protobuf.message.Message</li>
</ul>
<h3>Class variables</h3>
<dl>
<dt id="connpy.grpc_layer.connpy_pb2.MoveRequest.DESCRIPTOR"><code class="name">var <span class="ident">DESCRIPTOR</span></code></dt>
<dd>
<div class="desc"></div>
</dd>
</dl>
</dd>
<dt id="connpy.grpc_layer.connpy_pb2.NodeRequest"><code class="flex name class">
<span>class <span class="ident">NodeRequest</span></span>
<span>(</span><span>*args, **kwargs)</span>
</code></dt>
<dd>
<div class="desc"><p>A ProtocolMessage</p></div>
<h3>Ancestors</h3>
<ul class="hlist">
<li>google._upb._message.Message</li>
<li>google.protobuf.message.Message</li>
</ul>
<h3>Class variables</h3>
<dl>
<dt id="connpy.grpc_layer.connpy_pb2.NodeRequest.DESCRIPTOR"><code class="name">var <span class="ident">DESCRIPTOR</span></code></dt>
<dd>
<div class="desc"></div>
</dd>
</dl>
</dd>
<dt id="connpy.grpc_layer.connpy_pb2.NodeRunResult"><code class="flex name class">
<span>class <span class="ident">NodeRunResult</span></span>
<span>(</span><span>*args, **kwargs)</span>
</code></dt>
<dd>
<div class="desc"><p>A ProtocolMessage</p></div>
<h3>Ancestors</h3>
<ul class="hlist">
<li>google._upb._message.Message</li>
<li>google.protobuf.message.Message</li>
</ul>
<h3>Class variables</h3>
<dl>
<dt id="connpy.grpc_layer.connpy_pb2.NodeRunResult.DESCRIPTOR"><code class="name">var <span class="ident">DESCRIPTOR</span></code></dt>
<dd>
<div class="desc"></div>
</dd>
</dl>
</dd>
<dt id="connpy.grpc_layer.connpy_pb2.PluginRequest"><code class="flex name class">
<span>class <span class="ident">PluginRequest</span></span>
<span>(</span><span>*args, **kwargs)</span>
</code></dt>
<dd>
<div class="desc"><p>A ProtocolMessage</p></div>
<h3>Ancestors</h3>
<ul class="hlist">
<li>google._upb._message.Message</li>
<li>google.protobuf.message.Message</li>
</ul>
<h3>Class variables</h3>
<dl>
<dt id="connpy.grpc_layer.connpy_pb2.PluginRequest.DESCRIPTOR"><code class="name">var <span class="ident">DESCRIPTOR</span></code></dt>
<dd>
<div class="desc"></div>
</dd>
</dl>
</dd>
<dt id="connpy.grpc_layer.connpy_pb2.ProfileRequest"><code class="flex name class">
<span>class <span class="ident">ProfileRequest</span></span>
<span>(</span><span>*args, **kwargs)</span>
</code></dt>
<dd>
<div class="desc"><p>A ProtocolMessage</p></div>
<h3>Ancestors</h3>
<ul class="hlist">
<li>google._upb._message.Message</li>
<li>google.protobuf.message.Message</li>
</ul>
<h3>Class variables</h3>
<dl>
<dt id="connpy.grpc_layer.connpy_pb2.ProfileRequest.DESCRIPTOR"><code class="name">var <span class="ident">DESCRIPTOR</span></code></dt>
<dd>
<div class="desc"></div>
</dd>
</dl>
</dd>
<dt id="connpy.grpc_layer.connpy_pb2.ProviderRequest"><code class="flex name class">
<span>class <span class="ident">ProviderRequest</span></span>
<span>(</span><span>*args, **kwargs)</span>
</code></dt>
<dd>
<div class="desc"><p>A ProtocolMessage</p></div>
<h3>Ancestors</h3>
<ul class="hlist">
<li>google._upb._message.Message</li>
<li>google.protobuf.message.Message</li>
</ul>
<h3>Class variables</h3>
<dl>
<dt id="connpy.grpc_layer.connpy_pb2.ProviderRequest.DESCRIPTOR"><code class="name">var <span class="ident">DESCRIPTOR</span></code></dt>
<dd>
<div class="desc"></div>
</dd>
</dl>
</dd>
<dt id="connpy.grpc_layer.connpy_pb2.RunRequest"><code class="flex name class">
<span>class <span class="ident">RunRequest</span></span>
<span>(</span><span>*args, **kwargs)</span>
</code></dt>
<dd>
<div class="desc"><p>A ProtocolMessage</p></div>
<h3>Ancestors</h3>
<ul class="hlist">
<li>google._upb._message.Message</li>
<li>google.protobuf.message.Message</li>
</ul>
<h3>Class variables</h3>
<dl>
<dt id="connpy.grpc_layer.connpy_pb2.RunRequest.DESCRIPTOR"><code class="name">var <span class="ident">DESCRIPTOR</span></code></dt>
<dd>
<div class="desc"></div>
</dd>
</dl>
</dd>
<dt id="connpy.grpc_layer.connpy_pb2.ScriptRequest"><code class="flex name class">
<span>class <span class="ident">ScriptRequest</span></span>
<span>(</span><span>*args, **kwargs)</span>
</code></dt>
<dd>
<div class="desc"><p>A ProtocolMessage</p></div>
<h3>Ancestors</h3>
<ul class="hlist">
<li>google._upb._message.Message</li>
<li>google.protobuf.message.Message</li>
</ul>
<h3>Class variables</h3>
<dl>
<dt id="connpy.grpc_layer.connpy_pb2.ScriptRequest.DESCRIPTOR"><code class="name">var <span class="ident">DESCRIPTOR</span></code></dt>
<dd>
<div class="desc"></div>
</dd>
</dl>
</dd>
<dt id="connpy.grpc_layer.connpy_pb2.StringRequest"><code class="flex name class">
<span>class <span class="ident">StringRequest</span></span>
<span>(</span><span>*args, **kwargs)</span>
</code></dt>
<dd>
<div class="desc"><p>A ProtocolMessage</p></div>
<h3>Ancestors</h3>
<ul class="hlist">
<li>google._upb._message.Message</li>
<li>google.protobuf.message.Message</li>
</ul>
<h3>Class variables</h3>
<dl>
<dt id="connpy.grpc_layer.connpy_pb2.StringRequest.DESCRIPTOR"><code class="name">var <span class="ident">DESCRIPTOR</span></code></dt>
<dd>
<div class="desc"></div>
</dd>
</dl>
</dd>
<dt id="connpy.grpc_layer.connpy_pb2.StringResponse"><code class="flex name class">
<span>class <span class="ident">StringResponse</span></span>
<span>(</span><span>*args, **kwargs)</span>
</code></dt>
<dd>
<div class="desc"><p>A ProtocolMessage</p></div>
<h3>Ancestors</h3>
<ul class="hlist">
<li>google._upb._message.Message</li>
<li>google.protobuf.message.Message</li>
</ul>
<h3>Class variables</h3>
<dl>
<dt id="connpy.grpc_layer.connpy_pb2.StringResponse.DESCRIPTOR"><code class="name">var <span class="ident">DESCRIPTOR</span></code></dt>
<dd>
<div class="desc"></div>
</dd>
</dl>
</dd>
<dt id="connpy.grpc_layer.connpy_pb2.StructRequest"><code class="flex name class">
<span>class <span class="ident">StructRequest</span></span>
<span>(</span><span>*args, **kwargs)</span>
</code></dt>
<dd>
<div class="desc"><p>A ProtocolMessage</p></div>
<h3>Ancestors</h3>
<ul class="hlist">
<li>google._upb._message.Message</li>
<li>google.protobuf.message.Message</li>
</ul>
<h3>Class variables</h3>
<dl>
<dt id="connpy.grpc_layer.connpy_pb2.StructRequest.DESCRIPTOR"><code class="name">var <span class="ident">DESCRIPTOR</span></code></dt>
<dd>
<div class="desc"></div>
</dd>
</dl>
</dd>
<dt id="connpy.grpc_layer.connpy_pb2.StructResponse"><code class="flex name class">
<span>class <span class="ident">StructResponse</span></span>
<span>(</span><span>*args, **kwargs)</span>
</code></dt>
<dd>
<div class="desc"><p>A ProtocolMessage</p></div>
<h3>Ancestors</h3>
<ul class="hlist">
<li>google._upb._message.Message</li>
<li>google.protobuf.message.Message</li>
</ul>
<h3>Class variables</h3>
<dl>
<dt id="connpy.grpc_layer.connpy_pb2.StructResponse.DESCRIPTOR"><code class="name">var <span class="ident">DESCRIPTOR</span></code></dt>
<dd>
<div class="desc"></div>
</dd>
</dl>
</dd>
<dt id="connpy.grpc_layer.connpy_pb2.TestRequest"><code class="flex name class">
<span>class <span class="ident">TestRequest</span></span>
<span>(</span><span>*args, **kwargs)</span>
</code></dt>
<dd>
<div class="desc"><p>A ProtocolMessage</p></div>
<h3>Ancestors</h3>
<ul class="hlist">
<li>google._upb._message.Message</li>
<li>google.protobuf.message.Message</li>
</ul>
<h3>Class variables</h3>
<dl>
<dt id="connpy.grpc_layer.connpy_pb2.TestRequest.DESCRIPTOR"><code class="name">var <span class="ident">DESCRIPTOR</span></code></dt>
<dd>
<div class="desc"></div>
</dd>
</dl>
</dd>
<dt id="connpy.grpc_layer.connpy_pb2.UpdateRequest"><code class="flex name class">
<span>class <span class="ident">UpdateRequest</span></span>
<span>(</span><span>*args, **kwargs)</span>
</code></dt>
<dd>
<div class="desc"><p>A ProtocolMessage</p></div>
<h3>Ancestors</h3>
<ul class="hlist">
<li>google._upb._message.Message</li>
<li>google.protobuf.message.Message</li>
</ul>
<h3>Class variables</h3>
<dl>
<dt id="connpy.grpc_layer.connpy_pb2.UpdateRequest.DESCRIPTOR"><code class="name">var <span class="ident">DESCRIPTOR</span></code></dt>
<dd>
<div class="desc"></div>
</dd>
</dl>
</dd>
<dt id="connpy.grpc_layer.connpy_pb2.ValueResponse"><code class="flex name class">
<span>class <span class="ident">ValueResponse</span></span>
<span>(</span><span>*args, **kwargs)</span>
</code></dt>
<dd>
<div class="desc"><p>A ProtocolMessage</p></div>
<h3>Ancestors</h3>
<ul class="hlist">
<li>google._upb._message.Message</li>
<li>google.protobuf.message.Message</li>
</ul>
<h3>Class variables</h3>
<dl>
<dt id="connpy.grpc_layer.connpy_pb2.ValueResponse.DESCRIPTOR"><code class="name">var <span class="ident">DESCRIPTOR</span></code></dt>
<dd>
<div class="desc"></div>
</dd>
</dl>
</dd>
</dl>
</section> </section>
</article> </article>
<nav id="sidebar"> <nav id="sidebar">
@@ -668,207 +57,11 @@ el.replaceWith(d);
<li><code><a title="connpy.grpc_layer" href="index.html">connpy.grpc_layer</a></code></li> <li><code><a title="connpy.grpc_layer" href="index.html">connpy.grpc_layer</a></code></li>
</ul> </ul>
</li> </li>
<li><h3><a href="#header-classes">Classes</a></h3>
<ul>
<li>
<h4><code><a title="connpy.grpc_layer.connpy_pb2.AIResponse" href="#connpy.grpc_layer.connpy_pb2.AIResponse">AIResponse</a></code></h4>
<ul class="">
<li><code><a title="connpy.grpc_layer.connpy_pb2.AIResponse.DESCRIPTOR" href="#connpy.grpc_layer.connpy_pb2.AIResponse.DESCRIPTOR">DESCRIPTOR</a></code></li>
</ul>
</li>
<li>
<h4><code><a title="connpy.grpc_layer.connpy_pb2.AskRequest" href="#connpy.grpc_layer.connpy_pb2.AskRequest">AskRequest</a></code></h4>
<ul class="">
<li><code><a title="connpy.grpc_layer.connpy_pb2.AskRequest.DESCRIPTOR" href="#connpy.grpc_layer.connpy_pb2.AskRequest.DESCRIPTOR">DESCRIPTOR</a></code></li>
</ul>
</li>
<li>
<h4><code><a title="connpy.grpc_layer.connpy_pb2.BoolResponse" href="#connpy.grpc_layer.connpy_pb2.BoolResponse">BoolResponse</a></code></h4>
<ul class="">
<li><code><a title="connpy.grpc_layer.connpy_pb2.BoolResponse.DESCRIPTOR" href="#connpy.grpc_layer.connpy_pb2.BoolResponse.DESCRIPTOR">DESCRIPTOR</a></code></li>
</ul>
</li>
<li>
<h4><code><a title="connpy.grpc_layer.connpy_pb2.BulkRequest" href="#connpy.grpc_layer.connpy_pb2.BulkRequest">BulkRequest</a></code></h4>
<ul class="">
<li><code><a title="connpy.grpc_layer.connpy_pb2.BulkRequest.DESCRIPTOR" href="#connpy.grpc_layer.connpy_pb2.BulkRequest.DESCRIPTOR">DESCRIPTOR</a></code></li>
</ul>
</li>
<li>
<h4><code><a title="connpy.grpc_layer.connpy_pb2.CopilotRequest" href="#connpy.grpc_layer.connpy_pb2.CopilotRequest">CopilotRequest</a></code></h4>
<ul class="">
<li><code><a title="connpy.grpc_layer.connpy_pb2.CopilotRequest.DESCRIPTOR" href="#connpy.grpc_layer.connpy_pb2.CopilotRequest.DESCRIPTOR">DESCRIPTOR</a></code></li>
</ul>
</li>
<li>
<h4><code><a title="connpy.grpc_layer.connpy_pb2.CopilotResponse" href="#connpy.grpc_layer.connpy_pb2.CopilotResponse">CopilotResponse</a></code></h4>
<ul class="">
<li><code><a title="connpy.grpc_layer.connpy_pb2.CopilotResponse.DESCRIPTOR" href="#connpy.grpc_layer.connpy_pb2.CopilotResponse.DESCRIPTOR">DESCRIPTOR</a></code></li>
</ul>
</li>
<li>
<h4><code><a title="connpy.grpc_layer.connpy_pb2.DeleteRequest" href="#connpy.grpc_layer.connpy_pb2.DeleteRequest">DeleteRequest</a></code></h4>
<ul class="">
<li><code><a title="connpy.grpc_layer.connpy_pb2.DeleteRequest.DESCRIPTOR" href="#connpy.grpc_layer.connpy_pb2.DeleteRequest.DESCRIPTOR">DESCRIPTOR</a></code></li>
</ul>
</li>
<li>
<h4><code><a title="connpy.grpc_layer.connpy_pb2.ExportRequest" href="#connpy.grpc_layer.connpy_pb2.ExportRequest">ExportRequest</a></code></h4>
<ul class="">
<li><code><a title="connpy.grpc_layer.connpy_pb2.ExportRequest.DESCRIPTOR" href="#connpy.grpc_layer.connpy_pb2.ExportRequest.DESCRIPTOR">DESCRIPTOR</a></code></li>
</ul>
</li>
<li>
<h4><code><a title="connpy.grpc_layer.connpy_pb2.FilterRequest" href="#connpy.grpc_layer.connpy_pb2.FilterRequest">FilterRequest</a></code></h4>
<ul class="">
<li><code><a title="connpy.grpc_layer.connpy_pb2.FilterRequest.DESCRIPTOR" href="#connpy.grpc_layer.connpy_pb2.FilterRequest.DESCRIPTOR">DESCRIPTOR</a></code></li>
</ul>
</li>
<li>
<h4><code><a title="connpy.grpc_layer.connpy_pb2.FullReplaceRequest" href="#connpy.grpc_layer.connpy_pb2.FullReplaceRequest">FullReplaceRequest</a></code></h4>
<ul class="">
<li><code><a title="connpy.grpc_layer.connpy_pb2.FullReplaceRequest.DESCRIPTOR" href="#connpy.grpc_layer.connpy_pb2.FullReplaceRequest.DESCRIPTOR">DESCRIPTOR</a></code></li>
</ul>
</li>
<li>
<h4><code><a title="connpy.grpc_layer.connpy_pb2.IdRequest" href="#connpy.grpc_layer.connpy_pb2.IdRequest">IdRequest</a></code></h4>
<ul class="">
<li><code><a title="connpy.grpc_layer.connpy_pb2.IdRequest.DESCRIPTOR" href="#connpy.grpc_layer.connpy_pb2.IdRequest.DESCRIPTOR">DESCRIPTOR</a></code></li>
</ul>
</li>
<li>
<h4><code><a title="connpy.grpc_layer.connpy_pb2.IntRequest" href="#connpy.grpc_layer.connpy_pb2.IntRequest">IntRequest</a></code></h4>
<ul class="">
<li><code><a title="connpy.grpc_layer.connpy_pb2.IntRequest.DESCRIPTOR" href="#connpy.grpc_layer.connpy_pb2.IntRequest.DESCRIPTOR">DESCRIPTOR</a></code></li>
</ul>
</li>
<li>
<h4><code><a title="connpy.grpc_layer.connpy_pb2.InteractRequest" href="#connpy.grpc_layer.connpy_pb2.InteractRequest">InteractRequest</a></code></h4>
<ul class="">
<li><code><a title="connpy.grpc_layer.connpy_pb2.InteractRequest.DESCRIPTOR" href="#connpy.grpc_layer.connpy_pb2.InteractRequest.DESCRIPTOR">DESCRIPTOR</a></code></li>
</ul>
</li>
<li>
<h4><code><a title="connpy.grpc_layer.connpy_pb2.InteractResponse" href="#connpy.grpc_layer.connpy_pb2.InteractResponse">InteractResponse</a></code></h4>
<ul class="">
<li><code><a title="connpy.grpc_layer.connpy_pb2.InteractResponse.DESCRIPTOR" href="#connpy.grpc_layer.connpy_pb2.InteractResponse.DESCRIPTOR">DESCRIPTOR</a></code></li>
</ul>
</li>
<li>
<h4><code><a title="connpy.grpc_layer.connpy_pb2.ListRequest" href="#connpy.grpc_layer.connpy_pb2.ListRequest">ListRequest</a></code></h4>
<ul class="">
<li><code><a title="connpy.grpc_layer.connpy_pb2.ListRequest.DESCRIPTOR" href="#connpy.grpc_layer.connpy_pb2.ListRequest.DESCRIPTOR">DESCRIPTOR</a></code></li>
</ul>
</li>
<li>
<h4><code><a title="connpy.grpc_layer.connpy_pb2.MCPRequest" href="#connpy.grpc_layer.connpy_pb2.MCPRequest">MCPRequest</a></code></h4>
<ul class="">
<li><code><a title="connpy.grpc_layer.connpy_pb2.MCPRequest.DESCRIPTOR" href="#connpy.grpc_layer.connpy_pb2.MCPRequest.DESCRIPTOR">DESCRIPTOR</a></code></li>
</ul>
</li>
<li>
<h4><code><a title="connpy.grpc_layer.connpy_pb2.MessageValue" href="#connpy.grpc_layer.connpy_pb2.MessageValue">MessageValue</a></code></h4>
<ul class="">
<li><code><a title="connpy.grpc_layer.connpy_pb2.MessageValue.DESCRIPTOR" href="#connpy.grpc_layer.connpy_pb2.MessageValue.DESCRIPTOR">DESCRIPTOR</a></code></li>
</ul>
</li>
<li>
<h4><code><a title="connpy.grpc_layer.connpy_pb2.MoveRequest" href="#connpy.grpc_layer.connpy_pb2.MoveRequest">MoveRequest</a></code></h4>
<ul class="">
<li><code><a title="connpy.grpc_layer.connpy_pb2.MoveRequest.DESCRIPTOR" href="#connpy.grpc_layer.connpy_pb2.MoveRequest.DESCRIPTOR">DESCRIPTOR</a></code></li>
</ul>
</li>
<li>
<h4><code><a title="connpy.grpc_layer.connpy_pb2.NodeRequest" href="#connpy.grpc_layer.connpy_pb2.NodeRequest">NodeRequest</a></code></h4>
<ul class="">
<li><code><a title="connpy.grpc_layer.connpy_pb2.NodeRequest.DESCRIPTOR" href="#connpy.grpc_layer.connpy_pb2.NodeRequest.DESCRIPTOR">DESCRIPTOR</a></code></li>
</ul>
</li>
<li>
<h4><code><a title="connpy.grpc_layer.connpy_pb2.NodeRunResult" href="#connpy.grpc_layer.connpy_pb2.NodeRunResult">NodeRunResult</a></code></h4>
<ul class="">
<li><code><a title="connpy.grpc_layer.connpy_pb2.NodeRunResult.DESCRIPTOR" href="#connpy.grpc_layer.connpy_pb2.NodeRunResult.DESCRIPTOR">DESCRIPTOR</a></code></li>
</ul>
</li>
<li>
<h4><code><a title="connpy.grpc_layer.connpy_pb2.PluginRequest" href="#connpy.grpc_layer.connpy_pb2.PluginRequest">PluginRequest</a></code></h4>
<ul class="">
<li><code><a title="connpy.grpc_layer.connpy_pb2.PluginRequest.DESCRIPTOR" href="#connpy.grpc_layer.connpy_pb2.PluginRequest.DESCRIPTOR">DESCRIPTOR</a></code></li>
</ul>
</li>
<li>
<h4><code><a title="connpy.grpc_layer.connpy_pb2.ProfileRequest" href="#connpy.grpc_layer.connpy_pb2.ProfileRequest">ProfileRequest</a></code></h4>
<ul class="">
<li><code><a title="connpy.grpc_layer.connpy_pb2.ProfileRequest.DESCRIPTOR" href="#connpy.grpc_layer.connpy_pb2.ProfileRequest.DESCRIPTOR">DESCRIPTOR</a></code></li>
</ul>
</li>
<li>
<h4><code><a title="connpy.grpc_layer.connpy_pb2.ProviderRequest" href="#connpy.grpc_layer.connpy_pb2.ProviderRequest">ProviderRequest</a></code></h4>
<ul class="">
<li><code><a title="connpy.grpc_layer.connpy_pb2.ProviderRequest.DESCRIPTOR" href="#connpy.grpc_layer.connpy_pb2.ProviderRequest.DESCRIPTOR">DESCRIPTOR</a></code></li>
</ul>
</li>
<li>
<h4><code><a title="connpy.grpc_layer.connpy_pb2.RunRequest" href="#connpy.grpc_layer.connpy_pb2.RunRequest">RunRequest</a></code></h4>
<ul class="">
<li><code><a title="connpy.grpc_layer.connpy_pb2.RunRequest.DESCRIPTOR" href="#connpy.grpc_layer.connpy_pb2.RunRequest.DESCRIPTOR">DESCRIPTOR</a></code></li>
</ul>
</li>
<li>
<h4><code><a title="connpy.grpc_layer.connpy_pb2.ScriptRequest" href="#connpy.grpc_layer.connpy_pb2.ScriptRequest">ScriptRequest</a></code></h4>
<ul class="">
<li><code><a title="connpy.grpc_layer.connpy_pb2.ScriptRequest.DESCRIPTOR" href="#connpy.grpc_layer.connpy_pb2.ScriptRequest.DESCRIPTOR">DESCRIPTOR</a></code></li>
</ul>
</li>
<li>
<h4><code><a title="connpy.grpc_layer.connpy_pb2.StringRequest" href="#connpy.grpc_layer.connpy_pb2.StringRequest">StringRequest</a></code></h4>
<ul class="">
<li><code><a title="connpy.grpc_layer.connpy_pb2.StringRequest.DESCRIPTOR" href="#connpy.grpc_layer.connpy_pb2.StringRequest.DESCRIPTOR">DESCRIPTOR</a></code></li>
</ul>
</li>
<li>
<h4><code><a title="connpy.grpc_layer.connpy_pb2.StringResponse" href="#connpy.grpc_layer.connpy_pb2.StringResponse">StringResponse</a></code></h4>
<ul class="">
<li><code><a title="connpy.grpc_layer.connpy_pb2.StringResponse.DESCRIPTOR" href="#connpy.grpc_layer.connpy_pb2.StringResponse.DESCRIPTOR">DESCRIPTOR</a></code></li>
</ul>
</li>
<li>
<h4><code><a title="connpy.grpc_layer.connpy_pb2.StructRequest" href="#connpy.grpc_layer.connpy_pb2.StructRequest">StructRequest</a></code></h4>
<ul class="">
<li><code><a title="connpy.grpc_layer.connpy_pb2.StructRequest.DESCRIPTOR" href="#connpy.grpc_layer.connpy_pb2.StructRequest.DESCRIPTOR">DESCRIPTOR</a></code></li>
</ul>
</li>
<li>
<h4><code><a title="connpy.grpc_layer.connpy_pb2.StructResponse" href="#connpy.grpc_layer.connpy_pb2.StructResponse">StructResponse</a></code></h4>
<ul class="">
<li><code><a title="connpy.grpc_layer.connpy_pb2.StructResponse.DESCRIPTOR" href="#connpy.grpc_layer.connpy_pb2.StructResponse.DESCRIPTOR">DESCRIPTOR</a></code></li>
</ul>
</li>
<li>
<h4><code><a title="connpy.grpc_layer.connpy_pb2.TestRequest" href="#connpy.grpc_layer.connpy_pb2.TestRequest">TestRequest</a></code></h4>
<ul class="">
<li><code><a title="connpy.grpc_layer.connpy_pb2.TestRequest.DESCRIPTOR" href="#connpy.grpc_layer.connpy_pb2.TestRequest.DESCRIPTOR">DESCRIPTOR</a></code></li>
</ul>
</li>
<li>
<h4><code><a title="connpy.grpc_layer.connpy_pb2.UpdateRequest" href="#connpy.grpc_layer.connpy_pb2.UpdateRequest">UpdateRequest</a></code></h4>
<ul class="">
<li><code><a title="connpy.grpc_layer.connpy_pb2.UpdateRequest.DESCRIPTOR" href="#connpy.grpc_layer.connpy_pb2.UpdateRequest.DESCRIPTOR">DESCRIPTOR</a></code></li>
</ul>
</li>
<li>
<h4><code><a title="connpy.grpc_layer.connpy_pb2.ValueResponse" href="#connpy.grpc_layer.connpy_pb2.ValueResponse">ValueResponse</a></code></h4>
<ul class="">
<li><code><a title="connpy.grpc_layer.connpy_pb2.ValueResponse.DESCRIPTOR" href="#connpy.grpc_layer.connpy_pb2.ValueResponse.DESCRIPTOR">DESCRIPTOR</a></code></li>
</ul>
</li>
</ul>
</li>
</ul> </ul>
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.grpc_layer API documentation</title> <title>connpy.grpc_layer API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -102,7 +102,7 @@ el.replaceWith(d);
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.grpc_layer.remote_plugin_pb2 API documentation</title> <title>connpy.grpc_layer.remote_plugin_pb2 API documentation</title>
<meta name="description" content="Generated protocol buffer code."> <meta name="description" content="Generated protocol buffer code.">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -62,7 +62,7 @@ el.replaceWith(d);
<dl> <dl>
<dt id="connpy.grpc_layer.remote_plugin_pb2.IdRequest.DESCRIPTOR"><code class="name">var <span class="ident">DESCRIPTOR</span></code></dt> <dt id="connpy.grpc_layer.remote_plugin_pb2.IdRequest.DESCRIPTOR"><code class="name">var <span class="ident">DESCRIPTOR</span></code></dt>
<dd> <dd>
<div class="desc"></div> <div class="desc"><p>The type of the None singleton.</p></div>
</dd> </dd>
</dl> </dl>
</dd> </dd>
@@ -81,7 +81,7 @@ el.replaceWith(d);
<dl> <dl>
<dt id="connpy.grpc_layer.remote_plugin_pb2.OutputChunk.DESCRIPTOR"><code class="name">var <span class="ident">DESCRIPTOR</span></code></dt> <dt id="connpy.grpc_layer.remote_plugin_pb2.OutputChunk.DESCRIPTOR"><code class="name">var <span class="ident">DESCRIPTOR</span></code></dt>
<dd> <dd>
<div class="desc"></div> <div class="desc"><p>The type of the None singleton.</p></div>
</dd> </dd>
</dl> </dl>
</dd> </dd>
@@ -100,7 +100,7 @@ el.replaceWith(d);
<dl> <dl>
<dt id="connpy.grpc_layer.remote_plugin_pb2.PluginInvokeRequest.DESCRIPTOR"><code class="name">var <span class="ident">DESCRIPTOR</span></code></dt> <dt id="connpy.grpc_layer.remote_plugin_pb2.PluginInvokeRequest.DESCRIPTOR"><code class="name">var <span class="ident">DESCRIPTOR</span></code></dt>
<dd> <dd>
<div class="desc"></div> <div class="desc"><p>The type of the None singleton.</p></div>
</dd> </dd>
</dl> </dl>
</dd> </dd>
@@ -119,7 +119,7 @@ el.replaceWith(d);
<dl> <dl>
<dt id="connpy.grpc_layer.remote_plugin_pb2.StringResponse.DESCRIPTOR"><code class="name">var <span class="ident">DESCRIPTOR</span></code></dt> <dt id="connpy.grpc_layer.remote_plugin_pb2.StringResponse.DESCRIPTOR"><code class="name">var <span class="ident">DESCRIPTOR</span></code></dt>
<dd> <dd>
<div class="desc"></div> <div class="desc"><p>The type of the None singleton.</p></div>
</dd> </dd>
</dl> </dl>
</dd> </dd>
@@ -168,7 +168,7 @@ el.replaceWith(d);
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.grpc_layer.remote_plugin_pb2_grpc API documentation</title> <title>connpy.grpc_layer.remote_plugin_pb2_grpc API documentation</title>
<meta name="description" content="Client and server classes corresponding to protobuf-defined services."> <meta name="description" content="Client and server classes corresponding to protobuf-defined services.">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -366,7 +366,7 @@ def invoke_plugin(request,
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+29 -6
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.grpc_layer.server API documentation</title> <title>connpy.grpc_layer.server API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -177,9 +177,11 @@ el.replaceWith(d);
print(f&#34;AI Task Error: {e}&#34;) print(f&#34;AI Task Error: {e}&#34;)
traceback.print_exc() traceback.print_exc()
chunk_queue.put((&#34;status&#34;, f&#34;Error: {str(e)}&#34;)) chunk_queue.put((&#34;status&#34;, f&#34;Error: {str(e)}&#34;))
# Crucial: always send final_mark to avoid client deadlock
chunk_queue.put((&#34;final_mark&#34;, {&#34;response&#34;: f&#34;Error: {str(e)}&#34;, &#34;chat_history&#34;: history, &#34;error&#34;: True}))
def request_listener(): def request_listener():
nonlocal bridge, is_web, ai_thread, agent_instance nonlocal bridge, is_web, ai_thread, agent_instance, history
try: try:
for req in request_iterator: for req in request_iterator:
if req.interrupt: if req.interrupt:
@@ -193,12 +195,21 @@ el.replaceWith(d);
if req.input_text: if req.input_text:
is_web = &#34;web&#34; in (req.session_id or &#34;&#34;).lower() or (req.session_id or &#34;&#34;).lower().startswith(&#34;ws-&#34;) is_web = &#34;web&#34; in (req.session_id or &#34;&#34;).lower() or (req.session_id or &#34;&#34;).lower().startswith(&#34;ws-&#34;)
# Hydrate history from client if it&#39;s the first interaction in this stream
if not history and req.chat_history:
from .utils import from_value
history = from_value(req.chat_history) or []
if not bridge: if not bridge:
bridge = StatusBridge(chunk_queue, request_queue=request_queue, is_web=is_web) bridge = StatusBridge(chunk_queue, request_queue=request_queue, is_web=is_web)
overrides = {} overrides = {}
if req.engineer_model: overrides[&#34;engineer_model&#34;] = req.engineer_model if req.engineer_model: overrides[&#34;engineer_model&#34;] = req.engineer_model
if req.engineer_api_key: overrides[&#34;engineer_api_key&#34;] = req.engineer_api_key if req.engineer_api_key: overrides[&#34;engineer_api_key&#34;] = req.engineer_api_key
if req.architect_model: overrides[&#34;architect_model&#34;] = req.architect_model
if req.architect_api_key: overrides[&#34;architect_api_key&#34;] = req.architect_api_key
if req.HasField(&#34;engineer_auth&#34;): overrides[&#34;engineer_auth&#34;] = from_struct(req.engineer_auth)
if req.HasField(&#34;architect_auth&#34;): overrides[&#34;architect_auth&#34;] = from_struct(req.architect_auth)
# Start AI in its own thread so we can keep listening for interrupts # Start AI in its own thread so we can keep listening for interrupts
ai_thread = threading.Thread( ai_thread = threading.Thread(
@@ -263,7 +274,8 @@ el.replaceWith(d);
@handle_errors @handle_errors
def list_sessions(self, request, context): def list_sessions(self, request, context):
return connpy_pb2.ValueResponse(data=to_value(self.service.list_sessions())) sessions, total = self.service.list_sessions()
return connpy_pb2.ValueResponse(data=to_value(sessions))
@handle_errors @handle_errors
def delete_session(self, request, context): def delete_session(self, request, context):
@@ -272,7 +284,8 @@ el.replaceWith(d);
@handle_errors @handle_errors
def configure_provider(self, request, context): def configure_provider(self, request, context):
self.service.configure_provider(request.provider, request.model, request.api_key) auth_dict = from_struct(request.auth) if request.HasField(&#34;auth&#34;) else None
self.service.configure_provider(request.provider, request.model, request.api_key, auth=auth_dict)
return Empty() return Empty()
@handle_errors @handle_errors
@@ -286,6 +299,11 @@ el.replaceWith(d);
) )
return Empty() return Empty()
@handle_errors
def list_mcp_servers(self, request, context):
mcp_servers = self.service.list_mcp_servers()
return connpy_pb2.ValueResponse(data=to_value(mcp_servers))
@handle_errors @handle_errors
def load_session_data(self, request, context): def load_session_data(self, request, context):
return connpy_pb2.StructResponse(data=to_struct(self.service.load_session_data(request.value)))</code></pre> return connpy_pb2.StructResponse(data=to_struct(self.service.load_session_data(request.value)))</code></pre>
@@ -305,6 +323,7 @@ el.replaceWith(d);
<li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.AIServiceServicer.configure_provider" href="connpy_pb2_grpc.html#connpy.grpc_layer.connpy_pb2_grpc.AIServiceServicer.configure_provider">configure_provider</a></code></li> <li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.AIServiceServicer.configure_provider" href="connpy_pb2_grpc.html#connpy.grpc_layer.connpy_pb2_grpc.AIServiceServicer.configure_provider">configure_provider</a></code></li>
<li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.AIServiceServicer.confirm" href="connpy_pb2_grpc.html#connpy.grpc_layer.connpy_pb2_grpc.AIServiceServicer.confirm">confirm</a></code></li> <li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.AIServiceServicer.confirm" href="connpy_pb2_grpc.html#connpy.grpc_layer.connpy_pb2_grpc.AIServiceServicer.confirm">confirm</a></code></li>
<li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.AIServiceServicer.delete_session" href="connpy_pb2_grpc.html#connpy.grpc_layer.connpy_pb2_grpc.AIServiceServicer.delete_session">delete_session</a></code></li> <li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.AIServiceServicer.delete_session" href="connpy_pb2_grpc.html#connpy.grpc_layer.connpy_pb2_grpc.AIServiceServicer.delete_session">delete_session</a></code></li>
<li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.AIServiceServicer.list_mcp_servers" href="connpy_pb2_grpc.html#connpy.grpc_layer.connpy_pb2_grpc.AIServiceServicer.list_mcp_servers">list_mcp_servers</a></code></li>
<li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.AIServiceServicer.list_sessions" href="connpy_pb2_grpc.html#connpy.grpc_layer.connpy_pb2_grpc.AIServiceServicer.list_sessions">list_sessions</a></code></li> <li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.AIServiceServicer.list_sessions" href="connpy_pb2_grpc.html#connpy.grpc_layer.connpy_pb2_grpc.AIServiceServicer.list_sessions">list_sessions</a></code></li>
<li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.AIServiceServicer.load_session_data" href="connpy_pb2_grpc.html#connpy.grpc_layer.connpy_pb2_grpc.AIServiceServicer.load_session_data">load_session_data</a></code></li> <li><code><a title="connpy.grpc_layer.connpy_pb2_grpc.AIServiceServicer.load_session_data" href="connpy_pb2_grpc.html#connpy.grpc_layer.connpy_pb2_grpc.AIServiceServicer.load_session_data">load_session_data</a></code></li>
</ul> </ul>
@@ -975,7 +994,9 @@ interceptor chooses to service this RPC, or None otherwise.</p></div>
asyncio.run(n._async_interact_loop(remote_stream, resize_callback, copilot_handler=remote_copilot_handler)) asyncio.run(n._async_interact_loop(remote_stream, resize_callback, copilot_handler=remote_copilot_handler))
except Exception as e: except Exception as e:
pass import traceback
print(f&#34;[ERROR in run_async_loop] {e}&#34;)
traceback.print_exc()
finally: finally:
n._teardown_interact_environment() n._teardown_interact_environment()
response_queue.put(None) # Signal EOF response_queue.put(None) # Signal EOF
@@ -1195,6 +1216,7 @@ interceptor chooses to service this RPC, or None otherwise.</p></div>
self.service = ProfileService(config) self.service = ProfileService(config)
self.node_service = NodeService(config) self.node_service = NodeService(config)
@handle_errors @handle_errors
def list_profiles(self, request, context): def list_profiles(self, request, context):
f = request.filter_str if request.filter_str else None f = request.filter_str if request.filter_str else None
@@ -1261,6 +1283,7 @@ interceptor chooses to service this RPC, or None otherwise.</p></div>
self.on_interrupt = self._force_interrupt self.on_interrupt = self._force_interrupt
self.thread = None self.thread = None
self.is_web = is_web self.is_web = is_web
self.is_remote = True
def _force_interrupt(self): def _force_interrupt(self):
&#34;&#34;&#34;Forcefully raise KeyboardInterrupt in the target thread.&#34;&#34;&#34; &#34;&#34;&#34;Forcefully raise KeyboardInterrupt in the target thread.&#34;&#34;&#34;
@@ -1560,7 +1583,7 @@ interceptor chooses to service this RPC, or None otherwise.</p></div>
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+70 -13
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.grpc_layer.stubs API documentation</title> <title>connpy.grpc_layer.stubs API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -122,6 +122,10 @@ el.replaceWith(d);
) )
if chat_history is not None: if chat_history is not None:
initial_req.chat_history.CopyFrom(to_value(chat_history)) initial_req.chat_history.CopyFrom(to_value(chat_history))
if &#34;engineer_auth&#34; in overrides and overrides[&#34;engineer_auth&#34;]:
initial_req.engineer_auth.CopyFrom(to_struct(overrides[&#34;engineer_auth&#34;]))
if &#34;architect_auth&#34; in overrides and overrides[&#34;architect_auth&#34;]:
initial_req.architect_auth.CopyFrom(to_struct(overrides[&#34;architect_auth&#34;]))
req_queue.put(initial_req) req_queue.put(initial_req)
@@ -135,6 +139,7 @@ el.replaceWith(d);
full_content = &#34;&#34; full_content = &#34;&#34;
header_printed = False header_printed = False
current_responder = &#34;engineer&#34;
final_result = {&#34;response&#34;: &#34;&#34;, &#34;chat_history&#34;: []} final_result = {&#34;response&#34;: &#34;&#34;, &#34;chat_history&#34;: []}
# Background thread to pull responses from gRPC into a local queue # Background thread to pull responses from gRPC into a local queue
@@ -179,6 +184,10 @@ el.replaceWith(d);
break break
if response.status_update: if response.status_update:
if response.status_update.startswith(&#34;__RESPONDER__:&#34;):
current_responder = response.status_update.split(&#34;:&#34;)[1].lower()
continue
if response.requires_confirmation: if response.requires_confirmation:
if status: status.stop() if status: status.stop()
@@ -231,7 +240,9 @@ el.replaceWith(d);
stable_console = RichConsole(theme=connpy_theme, file=get_original_stdout()) stable_console = RichConsole(theme=connpy_theme, file=get_original_stdout())
# Print header on first chunk # Print header on first chunk
stable_console.print(Rule(&#34;[bold engineer]Network Engineer[/bold engineer]&#34;, style=&#34;engineer&#34;)) alias = &#34;architect&#34; if current_responder == &#34;architect&#34; else &#34;engineer&#34;
role_label = &#34;Network Architect&#34; if current_responder == &#34;architect&#34; else &#34;Network Engineer&#34;
stable_console.print(Rule(f&#34;[bold {alias}]{role_label}[/bold {alias}]&#34;, style=alias))
header_printed = True header_printed = True
# Initialize parser # Initialize parser
@@ -283,16 +294,23 @@ el.replaceWith(d);
return self.stub.confirm(connpy_pb2.StringRequest(value=input_text)).value return self.stub.confirm(connpy_pb2.StringRequest(value=input_text)).value
@handle_errors @handle_errors
def list_sessions(self): def list_sessions(self, limit=None):
return from_value(self.stub.list_sessions(Empty()).data) from .utils import from_value
res = self.stub.list_sessions(Empty())
sessions = from_value(res.data) or []
if limit and len(sessions) &gt; limit:
return sessions[:limit], len(sessions)
return sessions, len(sessions)
@handle_errors @handle_errors
def delete_session(self, session_id): def delete_session(self, session_id):
self.stub.delete_session(connpy_pb2.StringRequest(value=session_id)) self.stub.delete_session(connpy_pb2.StringRequest(value=session_id))
@handle_errors @handle_errors
def configure_provider(self, provider, model=None, api_key=None): def configure_provider(self, provider, model=None, api_key=None, auth=None):
req = connpy_pb2.ProviderRequest(provider=provider, model=model or &#34;&#34;, api_key=api_key or &#34;&#34;) req = connpy_pb2.ProviderRequest(provider=provider, model=model or &#34;&#34;, api_key=api_key or &#34;&#34;)
if auth:
req.auth.CopyFrom(to_struct(auth))
self.stub.configure_provider(req) self.stub.configure_provider(req)
@handle_errors @handle_errors
@@ -306,6 +324,11 @@ el.replaceWith(d);
) )
self.stub.configure_mcp(req) self.stub.configure_mcp(req)
@handle_errors
def list_mcp_servers(self):
res = self.stub.list_mcp_servers(Empty())
return from_value(res.data) or {}
@handle_errors @handle_errors
def load_session_data(self, session_id): def load_session_data(self, session_id):
return from_struct(self.stub.load_session_data(connpy_pb2.StringRequest(value=session_id)).data)</code></pre> return from_struct(self.stub.load_session_data(connpy_pb2.StringRequest(value=session_id)).data)</code></pre>
@@ -344,6 +367,10 @@ def ask(self, input_text, dryrun=False, chat_history=None, session_id=None, debu
) )
if chat_history is not None: if chat_history is not None:
initial_req.chat_history.CopyFrom(to_value(chat_history)) initial_req.chat_history.CopyFrom(to_value(chat_history))
if &#34;engineer_auth&#34; in overrides and overrides[&#34;engineer_auth&#34;]:
initial_req.engineer_auth.CopyFrom(to_struct(overrides[&#34;engineer_auth&#34;]))
if &#34;architect_auth&#34; in overrides and overrides[&#34;architect_auth&#34;]:
initial_req.architect_auth.CopyFrom(to_struct(overrides[&#34;architect_auth&#34;]))
req_queue.put(initial_req) req_queue.put(initial_req)
@@ -357,6 +384,7 @@ def ask(self, input_text, dryrun=False, chat_history=None, session_id=None, debu
full_content = &#34;&#34; full_content = &#34;&#34;
header_printed = False header_printed = False
current_responder = &#34;engineer&#34;
final_result = {&#34;response&#34;: &#34;&#34;, &#34;chat_history&#34;: []} final_result = {&#34;response&#34;: &#34;&#34;, &#34;chat_history&#34;: []}
# Background thread to pull responses from gRPC into a local queue # Background thread to pull responses from gRPC into a local queue
@@ -401,6 +429,10 @@ def ask(self, input_text, dryrun=False, chat_history=None, session_id=None, debu
break break
if response.status_update: if response.status_update:
if response.status_update.startswith(&#34;__RESPONDER__:&#34;):
current_responder = response.status_update.split(&#34;:&#34;)[1].lower()
continue
if response.requires_confirmation: if response.requires_confirmation:
if status: status.stop() if status: status.stop()
@@ -453,7 +485,9 @@ def ask(self, input_text, dryrun=False, chat_history=None, session_id=None, debu
stable_console = RichConsole(theme=connpy_theme, file=get_original_stdout()) stable_console = RichConsole(theme=connpy_theme, file=get_original_stdout())
# Print header on first chunk # Print header on first chunk
stable_console.print(Rule(&#34;[bold engineer]Network Engineer[/bold engineer]&#34;, style=&#34;engineer&#34;)) alias = &#34;architect&#34; if current_responder == &#34;architect&#34; else &#34;engineer&#34;
role_label = &#34;Network Architect&#34; if current_responder == &#34;architect&#34; else &#34;Network Engineer&#34;
stable_console.print(Rule(f&#34;[bold {alias}]{role_label}[/bold {alias}]&#34;, style=alias))
header_printed = True header_printed = True
# Initialize parser # Initialize parser
@@ -524,7 +558,7 @@ def configure_mcp(self, name, url=None, enabled=True, auto_load_on_os=None, remo
<div class="desc"></div> <div class="desc"></div>
</dd> </dd>
<dt id="connpy.grpc_layer.stubs.AIStub.configure_provider"><code class="name flex"> <dt id="connpy.grpc_layer.stubs.AIStub.configure_provider"><code class="name flex">
<span>def <span class="ident">configure_provider</span></span>(<span>self, provider, model=None, api_key=None)</span> <span>def <span class="ident">configure_provider</span></span>(<span>self, provider, model=None, api_key=None, auth=None)</span>
</code></dt> </code></dt>
<dd> <dd>
<details class="source"> <details class="source">
@@ -532,8 +566,10 @@ def configure_mcp(self, name, url=None, enabled=True, auto_load_on_os=None, remo
<span>Expand source code</span> <span>Expand source code</span>
</summary> </summary>
<pre><code class="python">@handle_errors <pre><code class="python">@handle_errors
def configure_provider(self, provider, model=None, api_key=None): def configure_provider(self, provider, model=None, api_key=None, auth=None):
req = connpy_pb2.ProviderRequest(provider=provider, model=model or &#34;&#34;, api_key=api_key or &#34;&#34;) req = connpy_pb2.ProviderRequest(provider=provider, model=model or &#34;&#34;, api_key=api_key or &#34;&#34;)
if auth:
req.auth.CopyFrom(to_struct(auth))
self.stub.configure_provider(req)</code></pre> self.stub.configure_provider(req)</code></pre>
</details> </details>
<div class="desc"></div> <div class="desc"></div>
@@ -566,8 +602,8 @@ def delete_session(self, session_id):
</details> </details>
<div class="desc"></div> <div class="desc"></div>
</dd> </dd>
<dt id="connpy.grpc_layer.stubs.AIStub.list_sessions"><code class="name flex"> <dt id="connpy.grpc_layer.stubs.AIStub.list_mcp_servers"><code class="name flex">
<span>def <span class="ident">list_sessions</span></span>(<span>self)</span> <span>def <span class="ident">list_mcp_servers</span></span>(<span>self)</span>
</code></dt> </code></dt>
<dd> <dd>
<details class="source"> <details class="source">
@@ -575,8 +611,28 @@ def delete_session(self, session_id):
<span>Expand source code</span> <span>Expand source code</span>
</summary> </summary>
<pre><code class="python">@handle_errors <pre><code class="python">@handle_errors
def list_sessions(self): def list_mcp_servers(self):
return from_value(self.stub.list_sessions(Empty()).data)</code></pre> res = self.stub.list_mcp_servers(Empty())
return from_value(res.data) or {}</code></pre>
</details>
<div class="desc"></div>
</dd>
<dt id="connpy.grpc_layer.stubs.AIStub.list_sessions"><code class="name flex">
<span>def <span class="ident">list_sessions</span></span>(<span>self, limit=None)</span>
</code></dt>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">@handle_errors
def list_sessions(self, limit=None):
from .utils import from_value
res = self.stub.list_sessions(Empty())
sessions = from_value(res.data) or []
if limit and len(sessions) &gt; limit:
return sessions[:limit], len(sessions)
return sessions, len(sessions)</code></pre>
</details> </details>
<div class="desc"></div> <div class="desc"></div>
</dd> </dd>
@@ -2470,6 +2526,7 @@ def stop_api(self):
<li><code><a title="connpy.grpc_layer.stubs.AIStub.configure_provider" href="#connpy.grpc_layer.stubs.AIStub.configure_provider">configure_provider</a></code></li> <li><code><a title="connpy.grpc_layer.stubs.AIStub.configure_provider" href="#connpy.grpc_layer.stubs.AIStub.configure_provider">configure_provider</a></code></li>
<li><code><a title="connpy.grpc_layer.stubs.AIStub.confirm" href="#connpy.grpc_layer.stubs.AIStub.confirm">confirm</a></code></li> <li><code><a title="connpy.grpc_layer.stubs.AIStub.confirm" href="#connpy.grpc_layer.stubs.AIStub.confirm">confirm</a></code></li>
<li><code><a title="connpy.grpc_layer.stubs.AIStub.delete_session" href="#connpy.grpc_layer.stubs.AIStub.delete_session">delete_session</a></code></li> <li><code><a title="connpy.grpc_layer.stubs.AIStub.delete_session" href="#connpy.grpc_layer.stubs.AIStub.delete_session">delete_session</a></code></li>
<li><code><a title="connpy.grpc_layer.stubs.AIStub.list_mcp_servers" href="#connpy.grpc_layer.stubs.AIStub.list_mcp_servers">list_mcp_servers</a></code></li>
<li><code><a title="connpy.grpc_layer.stubs.AIStub.list_sessions" href="#connpy.grpc_layer.stubs.AIStub.list_sessions">list_sessions</a></code></li> <li><code><a title="connpy.grpc_layer.stubs.AIStub.list_sessions" href="#connpy.grpc_layer.stubs.AIStub.list_sessions">list_sessions</a></code></li>
<li><code><a title="connpy.grpc_layer.stubs.AIStub.load_session_data" href="#connpy.grpc_layer.stubs.AIStub.load_session_data">load_session_data</a></code></li> <li><code><a title="connpy.grpc_layer.stubs.AIStub.load_session_data" href="#connpy.grpc_layer.stubs.AIStub.load_session_data">load_session_data</a></code></li>
</ul> </ul>
@@ -2561,7 +2618,7 @@ def stop_api(self):
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.grpc_layer.utils API documentation</title> <title>connpy.grpc_layer.utils API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -138,7 +138,7 @@ el.replaceWith(d);
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+157 -62
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy API documentation</title> <title>connpy API documentation</title>
<meta name="description" content="&lt;p align=&#34;center&#34;&gt; <meta name="description" content="&lt;p align=&#34;center&#34;&gt;
&lt;img src=&#34;https://nginx.gederico.dynu.net/images/CONNPY-resized.png&#34; alt=&#34;App Logo&#34;&gt; &lt;img src=&#34;https://nginx.gederico.dynu.net/images/CONNPY-resized.png&#34; alt=&#34;App Logo&#34;&gt;
@@ -205,10 +205,6 @@ response = myai.ask(&quot;What is the status of the BGP neighbors in the office?
<dd> <dd>
<div class="desc"></div> <div class="desc"></div>
</dd> </dd>
<dt><code class="name"><a title="connpy.tests" href="tests/index.html">connpy.tests</a></code></dt>
<dd>
<div class="desc"></div>
</dd>
<dt><code class="name"><a title="connpy.tunnels" href="tunnels.html">connpy.tunnels</a></code></dt> <dt><code class="name"><a title="connpy.tunnels" href="tunnels.html">connpy.tunnels</a></code></dt>
<dd> <dd>
<div class="desc"></div> <div class="desc"></div>
@@ -620,7 +616,7 @@ indicating successful verification.</p>
</dd> </dd>
<dt id="connpy.ai"><code class="flex name class"> <dt id="connpy.ai"><code class="flex name class">
<span>class <span class="ident">ai</span></span> <span>class <span class="ident">ai</span></span>
<span>(</span><span>config,<br>org=None,<br>api_key=None,<br>engineer_model=None,<br>architect_model=None,<br>engineer_api_key=None,<br>architect_api_key=None,<br>console=None,<br>confirm_handler=None,<br>trust=False)</span> <span>(</span><span>config,<br>org=None,<br>api_key=None,<br>engineer_model=None,<br>architect_model=None,<br>engineer_api_key=None,<br>architect_api_key=None,<br>console=None,<br>confirm_handler=None,<br>trust=False,<br>engineer_auth=None,<br>architect_auth=None,<br>**kwargs)</span>
</code></dt> </code></dt>
<dd> <dd>
<details class="source"> <details class="source">
@@ -638,7 +634,7 @@ class ai:
r&#39;^systemctl\s+status\s+&#39;, r&#39;^journalctl\s+&#39; r&#39;^systemctl\s+status\s+&#39;, r&#39;^journalctl\s+&#39;
] ]
def __init__(self, config, org=None, api_key=None, engineer_model=None, architect_model=None, engineer_api_key=None, architect_api_key=None, console=None, confirm_handler=None, trust=False): def __init__(self, config, org=None, api_key=None, engineer_model=None, architect_model=None, engineer_api_key=None, architect_api_key=None, console=None, confirm_handler=None, trust=False, engineer_auth=None, architect_auth=None, **kwargs):
self.config = config self.config = config
self.console = console or printer.console self.console = console or printer.console
self.confirm_handler = confirm_handler or self._local_confirm_handler self.confirm_handler = confirm_handler or self._local_confirm_handler
@@ -657,6 +653,29 @@ class ai:
self.engineer_key = engineer_api_key or aiconfig.get(&#34;engineer_api_key&#34;) self.engineer_key = engineer_api_key or aiconfig.get(&#34;engineer_api_key&#34;)
self.architect_key = architect_api_key or aiconfig.get(&#34;architect_api_key&#34;) self.architect_key = architect_api_key or aiconfig.get(&#34;architect_api_key&#34;)
# Auth configurations (Prioridad: Argumento -&gt; Config)
self.engineer_auth = engineer_auth if engineer_auth is not None else aiconfig.get(&#34;engineer_auth&#34;)
if self.engineer_auth is None:
self.engineer_auth = {}
elif not isinstance(self.engineer_auth, dict):
self.engineer_auth = {}
self.architect_auth = architect_auth if architect_auth is not None else aiconfig.get(&#34;architect_auth&#34;)
if self.architect_auth is None:
self.architect_auth = {}
elif not isinstance(self.architect_auth, dict):
self.architect_auth = {}
# Backward compatibility fallbacks: only inject api_key if the auth dict is empty/not configured
if self.engineer_key and not self.engineer_auth:
self.engineer_auth[&#34;api_key&#34;] = self.engineer_key
if self.architect_key and not self.architect_auth:
self.architect_auth[&#34;api_key&#34;] = self.architect_key
# Strategic Reasoning Engine (Architect) availability
is_architect_keyless = &#34;vertex&#34; in self.architect_model.lower() or &#34;ollama&#34; in self.architect_model.lower() or &#34;local&#34; in self.architect_model.lower()
self.has_architect = bool(self.architect_key or self.architect_auth or is_architect_keyless)
# Custom Trusted Commands Regexes # Custom Trusted Commands Regexes
custom_trusted = aiconfig.get(&#34;trusted_commands&#34;, []) custom_trusted = aiconfig.get(&#34;trusted_commands&#34;, [])
if isinstance(custom_trusted, str): if isinstance(custom_trusted, str):
@@ -697,12 +716,12 @@ class ai:
# Session Management # Session Management
self.sessions_dir = os.path.join(self.config.defaultdir, &#34;ai_sessions&#34;) self.sessions_dir = os.path.join(self.config.defaultdir, &#34;ai_sessions&#34;)
os.makedirs(self.sessions_dir, exist_ok=True) os.makedirs(self.sessions_dir, exist_ok=True)
self.session_id = None self.session_id = getattr(self.config, &#34;session_id&#34;, None)
self.session_path = None self.session_path = os.path.join(self.sessions_dir, f&#34;{self.session_id}.json&#34;) if self.session_id else None
# Prompts base agnósticos # Prompts base agnósticos
architect_instructions = &#34;&#34; architect_instructions = &#34;&#34;
if self.architect_key: if self.has_architect:
architect_instructions = &#34;&#34;&#34; architect_instructions = &#34;&#34;&#34;
CRITICAL - CONSULT vs ESCALATE: CRITICAL - CONSULT vs ESCALATE:
- ALWAYS use &#39;consult_architect&#39; for: Configuration planning, design decisions, complex troubleshooting. - ALWAYS use &#39;consult_architect&#39; for: Configuration planning, design decisions, complex troubleshooting.
@@ -718,7 +737,7 @@ class ai:
else: else:
architect_instructions = &#34;&#34;&#34; architect_instructions = &#34;&#34;&#34;
CRITICAL - ARCHITECT UNAVAILABLE: CRITICAL - ARCHITECT UNAVAILABLE:
- The Strategic Reasoning Engine (Architect) is currently UNAVAILABLE because its API key is not configured. - The Strategic Reasoning Engine (Architect) is currently UNAVAILABLE because its API key or authentication is not configured.
- DO NOT attempt to consult or escalate to the architect. - DO NOT attempt to consult or escalate to the architect.
- If the user asks to consult the architect, inform them that the Architect is offline and offer to help them directly to the best of your abilities. - If the user asks to consult the architect, inform them that the Architect is offline and offer to help them directly to the best of your abilities.
&#34;&#34;&#34; &#34;&#34;&#34;
@@ -824,15 +843,19 @@ class ai:
if status_formatter: if status_formatter:
self.tool_status_formatters[name] = status_formatter self.tool_status_formatters[name] = status_formatter
def _stream_completion(self, model, messages, tools, api_key, status=None, label=&#34;&#34;, debug=False, chunk_callback=None, **kwargs): def _stream_completion(self, model, messages, tools, api_key=None, status=None, label=&#34;&#34;, debug=False, chunk_callback=None, auth=None, **kwargs):
&#34;&#34;&#34;Stream a completion call, rendering styled Markdown in real-time. &#34;&#34;&#34;Stream a completion call, rendering styled Markdown in real-time.
Returns (response, streamed) where: Returns (response, streamed) where:
- response: reconstructed ModelResponse (same as non-streaming) - response: reconstructed ModelResponse (same as non-streaming)
- streamed: True if text was rendered to console during streaming - streamed: True if text was rendered to console during streaming
&#34;&#34;&#34; &#34;&#34;&#34;
auth_dict = auth if auth is not None else {}
if api_key and &#34;api_key&#34; not in auth_dict:
auth_dict = auth_dict.copy()
auth_dict[&#34;api_key&#34;] = api_key
stream_resp = completion(model=model, messages=messages, tools=tools, api_key=api_key, stream=True, **kwargs) stream_resp = completion(model=model, messages=messages, tools=tools, stream=True, **auth_dict, **kwargs)
chunks = [] chunks = []
full_content = &#34;&#34; full_content = &#34;&#34;
@@ -1275,7 +1298,7 @@ class ai:
try: try:
safe_messages = self._sanitize_messages(messages) safe_messages = self._sanitize_messages(messages)
response = completion(model=self.engineer_model, messages=safe_messages, tools=tools, api_key=self.engineer_key) response = completion(model=self.engineer_model, messages=safe_messages, tools=tools, **self.engineer_auth)
except Exception as e: except Exception as e:
if status: status.stop() if status: status.stop()
raise ValueError(f&#34;Engineer failed to connect: {str(e)}&#34;) raise ValueError(f&#34;Engineer failed to connect: {str(e)}&#34;)
@@ -1409,16 +1432,27 @@ class ai:
continue continue
return sorted(sessions, key=lambda x: x[&#34;created_at&#34;], reverse=True) return sorted(sessions, key=lambda x: x[&#34;created_at&#34;], reverse=True)
def list_sessions(self): def list_sessions(self, limit=20):
&#34;&#34;&#34;Prints a list of sessions using printer.table.&#34;&#34;&#34; &#34;&#34;&#34;Prints a list of sessions using printer.table.&#34;&#34;&#34;
sessions = self._get_sessions() sessions = self._get_sessions()
if not sessions: if not sessions:
printer.info(&#34;No saved AI sessions found.&#34;) printer.info(&#34;No saved AI sessions found.&#34;)
return return
total = len(sessions)
if limit and total &gt; limit:
sessions = sessions[:limit]
columns = [&#34;ID&#34;, &#34;Title&#34;, &#34;Created At&#34;, &#34;Model&#34;] columns = [&#34;ID&#34;, &#34;Title&#34;, &#34;Created At&#34;, &#34;Model&#34;]
rows = [[s[&#34;id&#34;], s[&#34;title&#34;], s[&#34;created_at&#34;], s[&#34;model&#34;]] for s in sessions] rows = [[s[&#34;id&#34;], s[&#34;title&#34;], s[&#34;created_at&#34;], s[&#34;model&#34;]] for s in sessions]
printer.table(&#34;AI Persisted Sessions&#34;, columns, rows)
title = &#34;AI Persisted Sessions&#34;
if limit and total &gt; limit:
title += f&#34; (Showing last {limit} of {total})&#34;
printer.table(title, columns, rows)
if limit and total &gt; limit:
printer.info(f&#34;Use &#39;--list --all&#39; (if supported) or check the sessions directory to see all {total} sessions.&#34;)
def load_session_data(self, session_id): def load_session_data(self, session_id):
&#34;&#34;&#34;Loads a session&#39;s raw data by ID.&#34;&#34;&#34; &#34;&#34;&#34;Loads a session&#39;s raw data by ID.&#34;&#34;&#34;
@@ -1449,8 +1483,10 @@ class ai:
return sessions[0][&#34;id&#34;] if sessions else None return sessions[0][&#34;id&#34;] if sessions else None
def _generate_session_id(self, query): def _generate_session_id(self, query):
&#34;&#34;&#34;Generates a unique session ID based on timestamp.&#34;&#34;&#34; &#34;&#34;&#34;Generates a unique session ID based on timestamp and a random suffix.&#34;&#34;&#34;
return datetime.datetime.now().strftime(&#34;%Y%m%d-%H%M%S&#34;) ts = datetime.datetime.now().strftime(&#34;%Y%m%d-%H%M%S&#34;)
suffix = secrets.token_hex(2)
return f&#34;{ts}-{suffix}&#34;
def save_session(self, history, title=None, model=None): def save_session(self, history, title=None, model=None):
&#34;&#34;&#34;Saves current history to the session file.&#34;&#34;&#34; &#34;&#34;&#34;Saves current history to the session file.&#34;&#34;&#34;
@@ -1459,6 +1495,8 @@ class ai:
first_user_msg = next((m[&#34;content&#34;] for m in history if m[&#34;role&#34;] == &#34;user&#34;), &#34;new-session&#34;) first_user_msg = next((m[&#34;content&#34;] for m in history if m[&#34;role&#34;] == &#34;user&#34;), &#34;new-session&#34;)
self.session_id = self._generate_session_id(first_user_msg) self.session_id = self._generate_session_id(first_user_msg)
self.session_path = os.path.join(self.sessions_dir, f&#34;{self.session_id}.json&#34;) self.session_path = os.path.join(self.sessions_dir, f&#34;{self.session_id}.json&#34;)
elif not self.session_path:
self.session_path = os.path.join(self.sessions_dir, f&#34;{self.session_id}.json&#34;)
# If it&#39;s a new file, we might want to set a better title # If it&#39;s a new file, we might want to set a better title
if not os.path.exists(self.session_path) and not title: if not os.path.exists(self.session_path) and not title:
@@ -1496,16 +1534,22 @@ 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):
if not self.engineer_key: is_engineer_keyless = &#34;vertex&#34; in self.engineer_model.lower() or &#34;ollama&#34; in self.engineer_model.lower() or &#34;local&#34; in self.engineer_model.lower()
raise ValueError(&#34;Engineer API key not configured. Use &#39;connpy config --engineer-api-key &lt;key&gt;&#39; to set it.&#34;) if not self.engineer_key and not self.engineer_auth and not is_engineer_keyless:
raise ValueError(&#34;Engineer API key or authentication not configured. Use &#39;connpy config --engineer-auth &lt;auth&gt;&#39; to set it.&#34;)
if chat_history is None: chat_history = [] if chat_history is None: chat_history = []
# Load session if provided and history is empty # Load session if provided and history is empty
if session_id and not chat_history: if session_id:
session_data = self.load_session_data(session_id) # Force the session_id even if it doesn&#39;t exist yet
if session_data: self.session_id = session_id
chat_history = session_data.get(&#34;history&#34;, []) self.session_path = os.path.join(self.sessions_dir, f&#34;{session_id}.json&#34;)
if not chat_history:
session_data = self.load_session_data(session_id)
if session_data:
chat_history = session_data.get(&#34;history&#34;, [])
# If we loaded history, the caller might need it back # If we loaded history, the caller might need it back
# But typically ask() is called in a loop with an external history object # But typically ask() is called in a loop with an external history object
@@ -1541,6 +1585,7 @@ class ai:
tools = self._get_architect_tools() if current_brain == &#34;architect&#34; else self._get_engineer_tools() tools = self._get_architect_tools() if current_brain == &#34;architect&#34; else self._get_engineer_tools()
model = self.architect_model if current_brain == &#34;architect&#34; else self.engineer_model model = self.architect_model if current_brain == &#34;architect&#34; else self.engineer_model
key = self.architect_key if current_brain == &#34;architect&#34; else self.engineer_key key = self.architect_key if current_brain == &#34;architect&#34; else self.engineer_key
current_auth = self.architect_auth if current_brain == &#34;architect&#34; else self.engineer_auth
# Estructura optimizada para Prompt Caching (Solo para Anthropic directo, Vertex tiene reglas distintas) # Estructura optimizada para Prompt Caching (Solo para Anthropic directo, Vertex tiene reglas distintas)
if &#34;claude&#34; in model.lower() and &#34;vertex&#34; not in model.lower(): if &#34;claude&#34; in model.lower() and &#34;vertex&#34; not in model.lower():
@@ -1590,8 +1635,8 @@ class ai:
label = &#34;[architect][bold]Architect[/bold][/architect]&#34; if current_brain == &#34;architect&#34; else &#34;[engineer][bold]Engineer[/bold][/engineer]&#34; label = &#34;[architect][bold]Architect[/bold][/architect]&#34; if current_brain == &#34;architect&#34; else &#34;[engineer][bold]Engineer[/bold][/engineer]&#34;
if status: if status:
# Notify responder identity ONLY for web/remote clients (StatusBridge has is_web) # Notify responder identity for web/remote clients
if getattr(status, &#34;is_web&#34;, False): if getattr(status, &#34;is_web&#34;, False) or getattr(status, &#34;is_remote&#34;, False):
status.update(f&#34;__RESPONDER__:{current_brain}&#34;) status.update(f&#34;__RESPONDER__:{current_brain}&#34;)
status.update(f&#34;{label} is thinking... (step {iteration})&#34;) status.update(f&#34;{label} is thinking... (step {iteration})&#34;)
@@ -1600,12 +1645,12 @@ class ai:
safe_messages = self._sanitize_messages(messages) safe_messages = self._sanitize_messages(messages)
if stream: if stream:
response, streamed_response = self._stream_completion( response, streamed_response = self._stream_completion(
model=model, messages=safe_messages, tools=tools, api_key=key, model=model, messages=safe_messages, tools=tools, auth=current_auth,
status=status, label=label, debug=debug, num_retries=3, status=status, label=label, debug=debug, num_retries=3,
chunk_callback=chunk_callback chunk_callback=chunk_callback
) )
else: else:
response = completion(model=model, messages=safe_messages, tools=tools, api_key=key, num_retries=3) 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 == &#34;architect&#34;: if current_brain == &#34;architect&#34;:
if status: status.update(&#34;[unavailable]Architect unavailable! Falling back to Engineer...&#34;) if status: status.update(&#34;[unavailable]Architect unavailable! Falling back to Engineer...&#34;)
@@ -1614,6 +1659,7 @@ class ai:
model = self.engineer_model model = self.engineer_model
tools = self._get_engineer_tools() tools = self._get_engineer_tools()
key = self.engineer_key key = self.engineer_key
current_auth = self.engineer_auth
# Rebuild messages with Engineer system prompt and original user request # Rebuild messages with Engineer system prompt and original user request
messages = [{&#34;role&#34;: &#34;system&#34;, &#34;content&#34;: self.engineer_system_prompt}] messages = [{&#34;role&#34;: &#34;system&#34;, &#34;content&#34;: self.engineer_system_prompt}]
# Add chat history if exists (excluding system prompt) # Add chat history if exists (excluding system prompt)
@@ -1706,6 +1752,7 @@ class ai:
model = self.architect_model model = self.architect_model
tools = self._get_architect_tools() tools = self._get_architect_tools()
key = self.architect_key key = self.architect_key
current_auth = self.architect_auth
messages[0] = {&#34;role&#34;: &#34;system&#34;, &#34;content&#34;: self.architect_system_prompt} messages[0] = {&#34;role&#34;: &#34;system&#34;, &#34;content&#34;: self.architect_system_prompt}
# Prepare handover context to inject AFTER all tool responses # Prepare handover context to inject AFTER all tool responses
handover_msg = f&#34;HANDOVER FROM EXECUTION ENGINE\n\nReason: {args[&#39;reason&#39;]}\n\nContext: {args[&#39;context&#39;]}\n\nYou are now in control of this conversation.&#34; handover_msg = f&#34;HANDOVER FROM EXECUTION ENGINE\n\nReason: {args[&#39;reason&#39;]}\n\nContext: {args[&#39;context&#39;]}\n\nYou are now in control of this conversation.&#34;
@@ -1727,6 +1774,7 @@ class ai:
model = self.engineer_model model = self.engineer_model
tools = self._get_engineer_tools() tools = self._get_engineer_tools()
key = self.engineer_key key = self.engineer_key
current_auth = self.engineer_auth
messages[0] = {&#34;role&#34;: &#34;system&#34;, &#34;content&#34;: self.engineer_system_prompt} messages[0] = {&#34;role&#34;: &#34;system&#34;, &#34;content&#34;: self.engineer_system_prompt}
# Prepare handover context to inject AFTER all tool responses # Prepare handover context to inject AFTER all tool responses
handover_msg = f&#34;HANDOVER FROM ARCHITECT\n\nSummary: {args[&#39;summary&#39;]}\n\nYou are now back in control. Continue handling the user&#39;s requests.&#34; handover_msg = f&#34;HANDOVER FROM ARCHITECT\n\nSummary: {args[&#39;summary&#39;]}\n\nYou are now back in control. Continue handling the user&#39;s requests.&#34;
@@ -1768,7 +1816,7 @@ class ai:
messages.append({&#34;role&#34;: &#34;user&#34;, &#34;content&#34;: &#34;Hard iteration limit reached. Please provide a summary of your findings so far.&#34;}) messages.append({&#34;role&#34;: &#34;user&#34;, &#34;content&#34;: &#34;Hard iteration limit reached. Please provide a summary of your findings so far.&#34;})
try: try:
safe_messages = self._sanitize_messages(messages) safe_messages = self._sanitize_messages(messages)
response = completion(model=model, messages=safe_messages, tools=[], api_key=key) response = completion(model=model, messages=safe_messages, tools=[], **current_auth)
resp_msg = response.choices[0].message resp_msg = response.choices[0].message
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:
@@ -1788,7 +1836,7 @@ class ai:
try: try:
safe_messages = self._sanitize_messages(summary_messages) safe_messages = self._sanitize_messages(summary_messages)
# Use tools=None to force a text summary during interruption # Use tools=None to force a text summary during interruption
response = completion(model=model, messages=safe_messages, tools=None, api_key=key) response = completion(model=model, messages=safe_messages, tools=None, **current_auth)
resp_msg = response.choices[0].message resp_msg = response.choices[0].message
messages.append(resp_msg.model_dump(exclude_none=True)) messages.append(resp_msg.model_dump(exclude_none=True))
@@ -1925,6 +1973,7 @@ Node: {node_name}&#34;&#34;&#34;
# Use models based on persona # Use models based on persona
current_model = self.architect_model if persona == &#34;architect&#34; else self.engineer_model current_model = self.architect_model if persona == &#34;architect&#34; else self.engineer_model
current_key = self.architect_key if persona == &#34;architect&#34; else self.engineer_key current_key = self.architect_key if persona == &#34;architect&#34; else self.engineer_key
current_auth = self.architect_auth if persona == &#34;architect&#34; else self.engineer_auth
try: try:
while iteration &lt; max_iterations: while iteration &lt; max_iterations:
@@ -1934,8 +1983,8 @@ Node: {node_name}&#34;&#34;&#34;
model=current_model, model=current_model,
messages=messages, messages=messages,
tools=mcp_tools if mcp_tools else None, tools=mcp_tools if mcp_tools else None,
api_key=current_key, stream=True,
stream=True **current_auth
) )
full_content = &#34;&#34; full_content = &#34;&#34;
@@ -2008,8 +2057,8 @@ Node: {node_name}&#34;&#34;&#34;
model=self.engineer_model, model=self.engineer_model,
messages=messages, messages=messages,
tools=None, tools=None,
api_key=self.engineer_key, stream=True,
stream=True **self.engineer_auth
) )
full_content = &#34;&#34; full_content = &#34;&#34;
@@ -2095,7 +2144,7 @@ Node: {node_name}&#34;&#34;&#34;
<dl> <dl>
<dt id="connpy.ai.SAFE_COMMANDS"><code class="name">var <span class="ident">SAFE_COMMANDS</span></code></dt> <dt id="connpy.ai.SAFE_COMMANDS"><code class="name">var <span class="ident">SAFE_COMMANDS</span></code></dt>
<dd> <dd>
<div class="desc"></div> <div class="desc"><p>The type of the None singleton.</p></div>
</dd> </dd>
</dl> </dl>
<h3>Instance variables</h3> <h3>Instance variables</h3>
@@ -2255,6 +2304,7 @@ Node: {node_name}&#34;&#34;&#34;
# Use models based on persona # Use models based on persona
current_model = self.architect_model if persona == &#34;architect&#34; else self.engineer_model current_model = self.architect_model if persona == &#34;architect&#34; else self.engineer_model
current_key = self.architect_key if persona == &#34;architect&#34; else self.engineer_key current_key = self.architect_key if persona == &#34;architect&#34; else self.engineer_key
current_auth = self.architect_auth if persona == &#34;architect&#34; else self.engineer_auth
try: try:
while iteration &lt; max_iterations: while iteration &lt; max_iterations:
@@ -2264,8 +2314,8 @@ Node: {node_name}&#34;&#34;&#34;
model=current_model, model=current_model,
messages=messages, messages=messages,
tools=mcp_tools if mcp_tools else None, tools=mcp_tools if mcp_tools else None,
api_key=current_key, stream=True,
stream=True **current_auth
) )
full_content = &#34;&#34; full_content = &#34;&#34;
@@ -2338,8 +2388,8 @@ Node: {node_name}&#34;&#34;&#34;
model=self.engineer_model, model=self.engineer_model,
messages=messages, messages=messages,
tools=None, tools=None,
api_key=self.engineer_key, stream=True,
stream=True **self.engineer_auth
) )
full_content = &#34;&#34; full_content = &#34;&#34;
@@ -2429,16 +2479,22 @@ Node: {node_name}&#34;&#34;&#34;
</summary> </summary>
<pre><code class="python">@MethodHook <pre><code class="python">@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):
if not self.engineer_key: is_engineer_keyless = &#34;vertex&#34; in self.engineer_model.lower() or &#34;ollama&#34; in self.engineer_model.lower() or &#34;local&#34; in self.engineer_model.lower()
raise ValueError(&#34;Engineer API key not configured. Use &#39;connpy config --engineer-api-key &lt;key&gt;&#39; to set it.&#34;) if not self.engineer_key and not self.engineer_auth and not is_engineer_keyless:
raise ValueError(&#34;Engineer API key or authentication not configured. Use &#39;connpy config --engineer-auth &lt;auth&gt;&#39; to set it.&#34;)
if chat_history is None: chat_history = [] if chat_history is None: chat_history = []
# Load session if provided and history is empty # Load session if provided and history is empty
if session_id and not chat_history: if session_id:
session_data = self.load_session_data(session_id) # Force the session_id even if it doesn&#39;t exist yet
if session_data: self.session_id = session_id
chat_history = session_data.get(&#34;history&#34;, []) self.session_path = os.path.join(self.sessions_dir, f&#34;{session_id}.json&#34;)
if not chat_history:
session_data = self.load_session_data(session_id)
if session_data:
chat_history = session_data.get(&#34;history&#34;, [])
# If we loaded history, the caller might need it back # If we loaded history, the caller might need it back
# But typically ask() is called in a loop with an external history object # But typically ask() is called in a loop with an external history object
@@ -2474,6 +2530,7 @@ def ask(self, user_input, dryrun=False, chat_history=None, status=None, debug=Fa
tools = self._get_architect_tools() if current_brain == &#34;architect&#34; else self._get_engineer_tools() tools = self._get_architect_tools() if current_brain == &#34;architect&#34; else self._get_engineer_tools()
model = self.architect_model if current_brain == &#34;architect&#34; else self.engineer_model model = self.architect_model if current_brain == &#34;architect&#34; else self.engineer_model
key = self.architect_key if current_brain == &#34;architect&#34; else self.engineer_key key = self.architect_key if current_brain == &#34;architect&#34; else self.engineer_key
current_auth = self.architect_auth if current_brain == &#34;architect&#34; else self.engineer_auth
# Estructura optimizada para Prompt Caching (Solo para Anthropic directo, Vertex tiene reglas distintas) # Estructura optimizada para Prompt Caching (Solo para Anthropic directo, Vertex tiene reglas distintas)
if &#34;claude&#34; in model.lower() and &#34;vertex&#34; not in model.lower(): if &#34;claude&#34; in model.lower() and &#34;vertex&#34; not in model.lower():
@@ -2523,8 +2580,8 @@ def ask(self, user_input, dryrun=False, chat_history=None, status=None, debug=Fa
label = &#34;[architect][bold]Architect[/bold][/architect]&#34; if current_brain == &#34;architect&#34; else &#34;[engineer][bold]Engineer[/bold][/engineer]&#34; label = &#34;[architect][bold]Architect[/bold][/architect]&#34; if current_brain == &#34;architect&#34; else &#34;[engineer][bold]Engineer[/bold][/engineer]&#34;
if status: if status:
# Notify responder identity ONLY for web/remote clients (StatusBridge has is_web) # Notify responder identity for web/remote clients
if getattr(status, &#34;is_web&#34;, False): if getattr(status, &#34;is_web&#34;, False) or getattr(status, &#34;is_remote&#34;, False):
status.update(f&#34;__RESPONDER__:{current_brain}&#34;) status.update(f&#34;__RESPONDER__:{current_brain}&#34;)
status.update(f&#34;{label} is thinking... (step {iteration})&#34;) status.update(f&#34;{label} is thinking... (step {iteration})&#34;)
@@ -2533,12 +2590,12 @@ def ask(self, user_input, dryrun=False, chat_history=None, status=None, debug=Fa
safe_messages = self._sanitize_messages(messages) safe_messages = self._sanitize_messages(messages)
if stream: if stream:
response, streamed_response = self._stream_completion( response, streamed_response = self._stream_completion(
model=model, messages=safe_messages, tools=tools, api_key=key, model=model, messages=safe_messages, tools=tools, auth=current_auth,
status=status, label=label, debug=debug, num_retries=3, status=status, label=label, debug=debug, num_retries=3,
chunk_callback=chunk_callback chunk_callback=chunk_callback
) )
else: else:
response = completion(model=model, messages=safe_messages, tools=tools, api_key=key, num_retries=3) 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 == &#34;architect&#34;: if current_brain == &#34;architect&#34;:
if status: status.update(&#34;[unavailable]Architect unavailable! Falling back to Engineer...&#34;) if status: status.update(&#34;[unavailable]Architect unavailable! Falling back to Engineer...&#34;)
@@ -2547,6 +2604,7 @@ def ask(self, user_input, dryrun=False, chat_history=None, status=None, debug=Fa
model = self.engineer_model model = self.engineer_model
tools = self._get_engineer_tools() tools = self._get_engineer_tools()
key = self.engineer_key key = self.engineer_key
current_auth = self.engineer_auth
# Rebuild messages with Engineer system prompt and original user request # Rebuild messages with Engineer system prompt and original user request
messages = [{&#34;role&#34;: &#34;system&#34;, &#34;content&#34;: self.engineer_system_prompt}] messages = [{&#34;role&#34;: &#34;system&#34;, &#34;content&#34;: self.engineer_system_prompt}]
# Add chat history if exists (excluding system prompt) # Add chat history if exists (excluding system prompt)
@@ -2639,6 +2697,7 @@ def ask(self, user_input, dryrun=False, chat_history=None, status=None, debug=Fa
model = self.architect_model model = self.architect_model
tools = self._get_architect_tools() tools = self._get_architect_tools()
key = self.architect_key key = self.architect_key
current_auth = self.architect_auth
messages[0] = {&#34;role&#34;: &#34;system&#34;, &#34;content&#34;: self.architect_system_prompt} messages[0] = {&#34;role&#34;: &#34;system&#34;, &#34;content&#34;: self.architect_system_prompt}
# Prepare handover context to inject AFTER all tool responses # Prepare handover context to inject AFTER all tool responses
handover_msg = f&#34;HANDOVER FROM EXECUTION ENGINE\n\nReason: {args[&#39;reason&#39;]}\n\nContext: {args[&#39;context&#39;]}\n\nYou are now in control of this conversation.&#34; handover_msg = f&#34;HANDOVER FROM EXECUTION ENGINE\n\nReason: {args[&#39;reason&#39;]}\n\nContext: {args[&#39;context&#39;]}\n\nYou are now in control of this conversation.&#34;
@@ -2660,6 +2719,7 @@ def ask(self, user_input, dryrun=False, chat_history=None, status=None, debug=Fa
model = self.engineer_model model = self.engineer_model
tools = self._get_engineer_tools() tools = self._get_engineer_tools()
key = self.engineer_key key = self.engineer_key
current_auth = self.engineer_auth
messages[0] = {&#34;role&#34;: &#34;system&#34;, &#34;content&#34;: self.engineer_system_prompt} messages[0] = {&#34;role&#34;: &#34;system&#34;, &#34;content&#34;: self.engineer_system_prompt}
# Prepare handover context to inject AFTER all tool responses # Prepare handover context to inject AFTER all tool responses
handover_msg = f&#34;HANDOVER FROM ARCHITECT\n\nSummary: {args[&#39;summary&#39;]}\n\nYou are now back in control. Continue handling the user&#39;s requests.&#34; handover_msg = f&#34;HANDOVER FROM ARCHITECT\n\nSummary: {args[&#39;summary&#39;]}\n\nYou are now back in control. Continue handling the user&#39;s requests.&#34;
@@ -2701,7 +2761,7 @@ def ask(self, user_input, dryrun=False, chat_history=None, status=None, debug=Fa
messages.append({&#34;role&#34;: &#34;user&#34;, &#34;content&#34;: &#34;Hard iteration limit reached. Please provide a summary of your findings so far.&#34;}) messages.append({&#34;role&#34;: &#34;user&#34;, &#34;content&#34;: &#34;Hard iteration limit reached. Please provide a summary of your findings so far.&#34;})
try: try:
safe_messages = self._sanitize_messages(messages) safe_messages = self._sanitize_messages(messages)
response = completion(model=model, messages=safe_messages, tools=[], api_key=key) response = completion(model=model, messages=safe_messages, tools=[], **current_auth)
resp_msg = response.choices[0].message resp_msg = response.choices[0].message
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:
@@ -2721,7 +2781,7 @@ def ask(self, user_input, dryrun=False, chat_history=None, status=None, debug=Fa
try: try:
safe_messages = self._sanitize_messages(summary_messages) safe_messages = self._sanitize_messages(summary_messages)
# Use tools=None to force a text summary during interruption # Use tools=None to force a text summary during interruption
response = completion(model=model, messages=safe_messages, tools=None, api_key=key) response = completion(model=model, messages=safe_messages, tools=None, **current_auth)
resp_msg = response.choices[0].message resp_msg = response.choices[0].message
messages.append(resp_msg.model_dump(exclude_none=True)) messages.append(resp_msg.model_dump(exclude_none=True))
@@ -2844,23 +2904,34 @@ def confirm(self, user_input): return True</code></pre>
<div class="desc"><p>List nodes matching the filter pattern. Returns metadata for &lt;=5 nodes, names only for more.</p></div> <div class="desc"><p>List nodes matching the filter pattern. Returns metadata for &lt;=5 nodes, names only for more.</p></div>
</dd> </dd>
<dt id="connpy.ai.list_sessions"><code class="name flex"> <dt id="connpy.ai.list_sessions"><code class="name flex">
<span>def <span class="ident">list_sessions</span></span>(<span>self)</span> <span>def <span class="ident">list_sessions</span></span>(<span>self, limit=20)</span>
</code></dt> </code></dt>
<dd> <dd>
<details class="source"> <details class="source">
<summary> <summary>
<span>Expand source code</span> <span>Expand source code</span>
</summary> </summary>
<pre><code class="python">def list_sessions(self): <pre><code class="python">def list_sessions(self, limit=20):
&#34;&#34;&#34;Prints a list of sessions using printer.table.&#34;&#34;&#34; &#34;&#34;&#34;Prints a list of sessions using printer.table.&#34;&#34;&#34;
sessions = self._get_sessions() sessions = self._get_sessions()
if not sessions: if not sessions:
printer.info(&#34;No saved AI sessions found.&#34;) printer.info(&#34;No saved AI sessions found.&#34;)
return return
total = len(sessions)
if limit and total &gt; limit:
sessions = sessions[:limit]
columns = [&#34;ID&#34;, &#34;Title&#34;, &#34;Created At&#34;, &#34;Model&#34;] columns = [&#34;ID&#34;, &#34;Title&#34;, &#34;Created At&#34;, &#34;Model&#34;]
rows = [[s[&#34;id&#34;], s[&#34;title&#34;], s[&#34;created_at&#34;], s[&#34;model&#34;]] for s in sessions] rows = [[s[&#34;id&#34;], s[&#34;title&#34;], s[&#34;created_at&#34;], s[&#34;model&#34;]] for s in sessions]
printer.table(&#34;AI Persisted Sessions&#34;, columns, rows)</code></pre>
title = &#34;AI Persisted Sessions&#34;
if limit and total &gt; limit:
title += f&#34; (Showing last {limit} of {total})&#34;
printer.table(title, columns, rows)
if limit and total &gt; limit:
printer.info(f&#34;Use &#39;--list --all&#39; (if supported) or check the sessions directory to see all {total} sessions.&#34;)</code></pre>
</details> </details>
<div class="desc"><p>Prints a list of sessions using printer.table.</p></div> <div class="desc"><p>Prints a list of sessions using printer.table.</p></div>
</dd> </dd>
@@ -3070,6 +3141,8 @@ def confirm(self, user_input): return True</code></pre>
first_user_msg = next((m[&#34;content&#34;] for m in history if m[&#34;role&#34;] == &#34;user&#34;), &#34;new-session&#34;) first_user_msg = next((m[&#34;content&#34;] for m in history if m[&#34;role&#34;] == &#34;user&#34;), &#34;new-session&#34;)
self.session_id = self._generate_session_id(first_user_msg) self.session_id = self._generate_session_id(first_user_msg)
self.session_path = os.path.join(self.sessions_dir, f&#34;{self.session_id}.json&#34;) self.session_path = os.path.join(self.sessions_dir, f&#34;{self.session_id}.json&#34;)
elif not self.session_path:
self.session_path = os.path.join(self.sessions_dir, f&#34;{self.session_id}.json&#34;)
# If it&#39;s a new file, we might want to set a better title # If it&#39;s a new file, we might want to set a better title
if not os.path.exists(self.session_path) and not title: if not os.path.exists(self.session_path) and not title:
@@ -4226,8 +4299,11 @@ class node:
def _setup_interact_environment(self, debug=False, logger=None, async_mode=False): def _setup_interact_environment(self, debug=False, logger=None, async_mode=False):
size = re.search(&#39;columns=([0-9]+).*lines=([0-9]+)&#39;,str(os.get_terminal_size())) try:
self.child.setwinsize(int(size.group(2)),int(size.group(1))) size = re.search(&#39;columns=([0-9]+).*lines=([0-9]+)&#39;,str(os.get_terminal_size()))
self.child.setwinsize(int(size.group(2)),int(size.group(1)))
except OSError:
pass
if logger: if logger:
port_str = f&#34;:{self.port}&#34; if self.port and self.protocol not in [&#34;ssm&#34;, &#34;kubectl&#34;, &#34;docker&#34;] else &#34;&#34; port_str = f&#34;:{self.port}&#34; if self.port and self.protocol not in [&#34;ssm&#34;, &#34;kubectl&#34;, &#34;docker&#34;] else &#34;&#34;
logger(&#34;success&#34;, f&#34;Connected to {self.unique} at {self.host}{port_str} via: {self.protocol}&#34;) logger(&#34;success&#34;, f&#34;Connected to {self.unique} at {self.host}{port_str} via: {self.protocol}&#34;)
@@ -4264,6 +4340,7 @@ class node:
async def _async_interact_loop(self, local_stream, resize_callback, copilot_handler=None): async def _async_interact_loop(self, local_stream, resize_callback, copilot_handler=None):
local_stream.setup(resize_callback=resize_callback) local_stream.setup(resize_callback=resize_callback)
self.current_local_stream = local_stream
try: try:
child_fd = self.child.child_fd child_fd = self.child.child_fd
@@ -4346,11 +4423,19 @@ class node:
# Remove any stray \x00 bytes and forward normally # Remove any stray \x00 bytes and forward normally
clean_data = data.replace(b&#39;\x00&#39;, b&#39;&#39;) clean_data = data.replace(b&#39;\x00&#39;, b&#39;&#39;)
if clean_data: if clean_data:
# Track command boundaries when user hits Enter # Track command boundaries when user hits Enter or presses Ctrl+C
if hasattr(self, &#39;mylog&#39;) and (b&#39;\r&#39; in clean_data or b&#39;\n&#39; in clean_data): if hasattr(self, &#39;mylog&#39;) and (b&#39;\r&#39; in clean_data or b&#39;\n&#39; in clean_data or b&#39;\x03&#39; in clean_data):
self.cmd_byte_positions.append((self.mylog.tell(), None)) pos = self.mylog.tell()
marker_cmd = &#34;CANCELLED&#34; if b&#39;\x03&#39; in clean_data else None
self.cmd_byte_positions.append((pos, marker_cmd))
if hasattr(self, &#39;current_local_stream&#39;) and self.current_local_stream is not None:
try:
await self.current_local_stream.write(f&#39;\x1b]133;B;{pos}\x07&#39;.encode())
except Exception:
pass
try: os.write(child_fd, clean_data) try:
os.write(child_fd, clean_data)
except OSError: except OSError:
break break
self.lastinput = time() self.lastinput = time()
@@ -4470,6 +4555,7 @@ class node:
except Exception: except Exception:
pass pass
finally: finally:
self.current_local_stream = None
local_stream.teardown() local_stream.teardown()
@MethodHook @MethodHook
@@ -4498,6 +4584,11 @@ class node:
if cmd != slc and hasattr(self, &#39;cmd_byte_positions&#39;) and self.cmd_byte_positions is not None: if cmd != slc and hasattr(self, &#39;cmd_byte_positions&#39;) and self.cmd_byte_positions is not None:
log_pos = self.mylog.tell() if hasattr(self, &#39;mylog&#39;) else 0 log_pos = self.mylog.tell() if hasattr(self, &#39;mylog&#39;) else 0
self.cmd_byte_positions.append((log_pos, cmd)) self.cmd_byte_positions.append((log_pos, cmd))
if hasattr(self, &#39;current_local_stream&#39;) and self.current_local_stream is not None:
try:
await self.current_local_stream.write(f&#39;\x1b]133;B;{log_pos}\x07&#39;.encode())
except Exception:
pass
# Write physically to PTY # Write physically to PTY
os.write(child_fd, (cmd + &#34;\n&#34;).encode()) os.write(child_fd, (cmd + &#34;\n&#34;).encode())
@@ -5137,6 +5228,11 @@ async def inject_commands(self, commands, child_fd, on_inject=None):
if cmd != slc and hasattr(self, &#39;cmd_byte_positions&#39;) and self.cmd_byte_positions is not None: if cmd != slc and hasattr(self, &#39;cmd_byte_positions&#39;) and self.cmd_byte_positions is not None:
log_pos = self.mylog.tell() if hasattr(self, &#39;mylog&#39;) else 0 log_pos = self.mylog.tell() if hasattr(self, &#39;mylog&#39;) else 0
self.cmd_byte_positions.append((log_pos, cmd)) self.cmd_byte_positions.append((log_pos, cmd))
if hasattr(self, &#39;current_local_stream&#39;) and self.current_local_stream is not None:
try:
await self.current_local_stream.write(f&#39;\x1b]133;B;{log_pos}\x07&#39;.encode())
except Exception:
pass
# Write physically to PTY # Write physically to PTY
os.write(child_fd, (cmd + &#34;\n&#34;).encode()) os.write(child_fd, (cmd + &#34;\n&#34;).encode())
@@ -6225,7 +6321,6 @@ def test(self, commands, expected, vars = None,*, folder = None, prompt = None,
<li><code><a title="connpy.mcp_client" href="mcp_client.html">connpy.mcp_client</a></code></li> <li><code><a title="connpy.mcp_client" href="mcp_client.html">connpy.mcp_client</a></code></li>
<li><code><a title="connpy.proto" href="proto/index.html">connpy.proto</a></code></li> <li><code><a title="connpy.proto" href="proto/index.html">connpy.proto</a></code></li>
<li><code><a title="connpy.services" href="services/index.html">connpy.services</a></code></li> <li><code><a title="connpy.services" href="services/index.html">connpy.services</a></code></li>
<li><code><a title="connpy.tests" href="tests/index.html">connpy.tests</a></code></li>
<li><code><a title="connpy.tunnels" href="tunnels.html">connpy.tunnels</a></code></li> <li><code><a title="connpy.tunnels" href="tunnels.html">connpy.tunnels</a></code></li>
<li><code><a title="connpy.utils" href="utils.html">connpy.utils</a></code></li> <li><code><a title="connpy.utils" href="utils.html">connpy.utils</a></code></li>
</ul> </ul>
@@ -6289,7 +6384,7 @@ def test(self, commands, expected, vars = None,*, folder = None, prompt = None,
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.mcp_client API documentation</title> <title>connpy.mcp_client API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -343,7 +343,7 @@ el.replaceWith(d);
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.proto API documentation</title> <title>connpy.proto API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -60,7 +60,7 @@ el.replaceWith(d);
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+200 -56
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.services.ai_service API documentation</title> <title>connpy.services.ai_service API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -58,6 +58,37 @@ el.replaceWith(d);
<pre><code class="python">class AIService(BaseService): <pre><code class="python">class AIService(BaseService):
&#34;&#34;&#34;Business logic for interacting with AI agents and LLM configurations.&#34;&#34;&#34; &#34;&#34;&#34;Business logic for interacting with AI agents and LLM configurations.&#34;&#34;&#34;
def _clean_cisco_scrolling(self, text: str) -&gt; str:
&#34;&#34;&#34;Resolves horizontal scrolling artifacts (backspaces, \r, ANSI) by merging overlapping segments.&#34;&#34;&#34;
def merge_overlapping(s1, s2):
s2_clean = s2.lstrip(&#39; $&#39;)
max_overlap = min(len(s1), len(s2_clean))
for i in range(max_overlap, 0, -1):
if s1[-i:] == s2_clean[:i]:
return s1 + s2_clean[i:]
return s1 + s2_clean
scroll_re = re.compile(r&#39;(\x08{5,}\s*\$?|\$\r|\x1b\[\d+[GD]\s*\$?)&#39;)
parts = scroll_re.split(text)
merged = &#34;&#34;
for part in parts:
if scroll_re.match(part):
continue
cleaned = log_cleaner(part)
if not merged:
merged = cleaned
else:
merged_lines = merged.split(&#39;\n&#39;)
cleaned_lines = cleaned.split(&#39;\n&#39;)
merged_lines[-1] = merge_overlapping(merged_lines[-1], cleaned_lines[0])
merged_lines.extend(cleaned_lines[1:])
merged = &#34;\n&#34;.join(merged_lines)
return merged
def build_context_blocks(self, raw_bytes: bytes, cmd_byte_positions: list, node_info: dict, last_line: str = &#34;&#34;) -&gt; list: def build_context_blocks(self, raw_bytes: bytes, cmd_byte_positions: list, node_info: dict, last_line: str = &#34;&#34;) -&gt; list:
&#34;&#34;&#34;Identifies command blocks in the terminal history.&#34;&#34;&#34; &#34;&#34;&#34;Identifies command blocks in the terminal history.&#34;&#34;&#34;
blocks = [] blocks = []
@@ -79,28 +110,69 @@ el.replaceWith(d);
prev_pos = cmd_byte_positions[i-1][0] prev_pos = cmd_byte_positions[i-1][0]
if known_cmd: if known_cmd:
prev_chunk = raw_bytes[prev_pos:pos] if known_cmd == &#34;CANCELLED&#34;:
prev_cleaned = log_cleaner(prev_chunk.decode(errors=&#39;replace&#39;)) parsed_positions.append({&#34;pos&#34;: pos, &#34;type&#34;: &#34;CANCELLED&#34;, &#34;preview&#34;: &#34;&#34;})
prev_lines = [l for l in prev_cleaned.split(&#39;\n&#39;) if l.strip()] else:
prompt_text = prev_lines[-1].strip() if prev_lines else &#34;&#34; prev_chunk = raw_bytes[prev_pos:pos]
preview = f&#34;{prompt_text}{known_cmd}&#34; if prompt_text else known_cmd prev_cleaned = self._clean_cisco_scrolling(prev_chunk.decode(errors=&#39;replace&#39;))
parsed_positions.append({&#34;pos&#34;: pos, &#34;type&#34;: &#34;VALID_CMD&#34;, &#34;preview&#34;: preview[:80]}) prev_lines = [l for l in prev_cleaned.split(&#39;\n&#39;) if l.strip()]
prompt_text = prev_lines[-1].strip() if prev_lines else &#34;&#34;
preview = f&#34;{prompt_text}{known_cmd}&#34; if prompt_text else known_cmd
if len(preview) &gt; 80:
preview = preview[:77] + &#34;...&#34;
parsed_positions.append({&#34;pos&#34;: pos, &#34;type&#34;: &#34;VALID_CMD&#34;, &#34;preview&#34;: preview})
else: else:
chunk = raw_bytes[prev_pos:pos] chunk = raw_bytes[prev_pos:pos]
cleaned = log_cleaner(chunk.decode(errors=&#39;replace&#39;))
lines = [l for l in cleaned.split(&#39;\n&#39;) if l.strip()]
preview = lines[-1].strip() if lines else &#34;&#34;
if preview: cleaned = self._clean_cisco_scrolling(chunk.decode(errors=&#39;replace&#39;))
match = prompt_re.search(preview) lines = [l for l in cleaned.split(&#39;\n&#39;) if l.strip()]
if match:
cmd_text = preview[match.end():].strip() found_in_pass1 = False
if cmd_text: if lines:
parsed_positions.append({&#34;pos&#34;: pos, &#34;type&#34;: &#34;VALID_CMD&#34;, &#34;preview&#34;: preview[:80]}) # Search backwards through the last few lines for the prompt
else: for idx in range(len(lines) - 1, max(-1, len(lines) - 10), -1):
parsed_positions.append({&#34;pos&#34;: pos, &#34;type&#34;: &#34;EMPTY_PROMPT&#34;, &#34;preview&#34;: &#34;&#34;}) match = prompt_re.search(lines[idx])
else: if match:
parsed_positions.append({&#34;pos&#34;: pos, &#34;type&#34;: &#34;SCROLLING&#34;, &#34;preview&#34;: &#34;&#34;}) ptxt = match.group(0).strip()
cmd_first_line = lines[idx][match.end():].strip()
cmd_rest = [l.strip() for l in lines[idx+1:]]
cmd_text = &#34; &#34;.join([cmd_first_line] + cmd_rest).strip()
if cmd_text:
pv = f&#34;{ptxt} {cmd_text}&#34;.strip()
if len(pv) &gt; 80:
pv = pv[:77] + &#34;...&#34;
parsed_positions.append({&#34;pos&#34;: pos, &#34;type&#34;: &#34;VALID_CMD&#34;, &#34;preview&#34;: pv})
else:
parsed_positions.append({&#34;pos&#34;: pos, &#34;type&#34;: &#34;EMPTY_PROMPT&#34;, &#34;preview&#34;: &#34;&#34;})
found_in_pass1 = True
break
if not found_in_pass1:
# Fallback: The prompt might have been isolated in the previous chunk
# due to asynchronous network delays splitting the output exactly at the newline.
prev_was_valid_cmd = i &gt;= 2 and parsed_positions[i-2][&#34;type&#34;] == &#34;VALID_CMD&#34;
if prev_pos &gt; 0 and not prev_was_valid_cmd:
# Fetch the very last chunk that we just processed
prev_prev_pos = cmd_byte_positions[i-2][0] if i &gt;= 2 else 0
prev_chunk_text = self._clean_cisco_scrolling(raw_bytes[prev_prev_pos:prev_pos].decode(errors=&#39;replace&#39;))
prev_lines_text = [l for l in prev_chunk_text.split(&#39;\n&#39;) if l.strip()]
if prev_lines_text:
prev_match = prompt_re.search(prev_lines_text[-1])
if prev_match:
ptxt = prev_match.group(0).strip()
cmd_text = &#34; &#34;.join([l.strip() for l in lines]).strip()
if cmd_text:
pv = f&#34;{ptxt} {cmd_text}&#34;.strip()
if len(pv) &gt; 80:
pv = pv[:77] + &#34;...&#34;
parsed_positions.append({&#34;pos&#34;: pos, &#34;type&#34;: &#34;VALID_CMD&#34;, &#34;preview&#34;: pv})
found_in_pass1 = True
if not found_in_pass1:
parsed_positions.append({&#34;pos&#34;: pos, &#34;type&#34;: &#34;SCROLLING&#34;, &#34;preview&#34;: &#34;&#34;})
else: else:
parsed_positions.append({&#34;pos&#34;: pos, &#34;type&#34;: &#34;SCROLLING&#34;, &#34;preview&#34;: &#34;&#34;}) parsed_positions.append({&#34;pos&#34;: pos, &#34;type&#34;: &#34;SCROLLING&#34;, &#34;preview&#34;: &#34;&#34;})
@@ -113,11 +185,11 @@ el.replaceWith(d);
start_pos = item[&#34;pos&#34;] start_pos = item[&#34;pos&#34;]
preview = item[&#34;preview&#34;] preview = item[&#34;preview&#34;]
# Find the end position: next VALID_CMD or EMPTY_PROMPT # Find the end position: next VALID_CMD or EMPTY_PROMPT or CANCELLED
end_pos = current_prompt_pos end_pos = current_prompt_pos
for j in range(i + 1, len(parsed_positions)): for j in range(i + 1, len(parsed_positions)):
next_item = parsed_positions[j] next_item = parsed_positions[j]
if next_item[&#34;type&#34;] in (&#34;VALID_CMD&#34;, &#34;EMPTY_PROMPT&#34;): if next_item[&#34;type&#34;] in (&#34;VALID_CMD&#34;, &#34;EMPTY_PROMPT&#34;, &#34;CANCELLED&#34;):
end_pos = next_item[&#34;pos&#34;] end_pos = next_item[&#34;pos&#34;]
break break
@@ -219,11 +291,14 @@ el.replaceWith(d);
return await asyncio.wrap_future(future) return await asyncio.wrap_future(future)
def list_sessions(self): def list_sessions(self, limit=None):
&#34;&#34;&#34;Return a list of all saved AI sessions.&#34;&#34;&#34; &#34;&#34;&#34;Return a list of saved AI sessions, optionally limited.&#34;&#34;&#34;
from connpy.ai import ai from connpy.ai import ai
agent = ai(self.config) agent = ai(self.config)
return agent._get_sessions() sessions = agent._get_sessions()
if limit and len(sessions) &gt; limit:
return sessions[:limit], len(sessions)
return sessions, len(sessions)
def delete_session(self, session_id): def delete_session(self, session_id):
&#34;&#34;&#34;Delete an AI session by ID.&#34;&#34;&#34; &#34;&#34;&#34;Delete an AI session by ID.&#34;&#34;&#34;
@@ -235,13 +310,15 @@ el.replaceWith(d);
else: else:
raise InvalidConfigurationError(f&#34;Session &#39;{session_id}&#39; not found.&#34;) raise InvalidConfigurationError(f&#34;Session &#39;{session_id}&#39; not found.&#34;)
def configure_provider(self, provider, model=None, api_key=None): def configure_provider(self, provider, model=None, api_key=None, auth=None):
&#34;&#34;&#34;Update AI provider settings in the configuration.&#34;&#34;&#34; &#34;&#34;&#34;Update AI provider settings in the configuration.&#34;&#34;&#34;
settings = self.config.config.get(&#34;ai&#34;, {}) settings = self.config.config.get(&#34;ai&#34;, {})
if model: if model:
settings[f&#34;{provider}_model&#34;] = model settings[f&#34;{provider}_model&#34;] = model
if api_key: if api_key:
settings[f&#34;{provider}_api_key&#34;] = api_key settings[f&#34;{provider}_api_key&#34;] = api_key
if auth is not None:
settings[f&#34;{provider}_auth&#34;] = auth
self.config.config[&#34;ai&#34;] = settings self.config.config[&#34;ai&#34;] = settings
self.config._saveconfig(self.config.file) self.config._saveconfig(self.config.file)
@@ -280,6 +357,11 @@ el.replaceWith(d);
self.config.config[&#34;ai&#34;] = ai_settings self.config.config[&#34;ai&#34;] = ai_settings
self.config._saveconfig(self.config.file) self.config._saveconfig(self.config.file)
def list_mcp_servers(self) -&gt; dict:
&#34;&#34;&#34;Get the configured MCP servers.&#34;&#34;&#34;
ai_settings = self.config.config.get(&#34;ai&#34;, {})
return ai_settings.get(&#34;mcp_servers&#34;, {})
def load_session_data(self, session_id): def load_session_data(self, session_id):
&#34;&#34;&#34;Load a session&#39;s raw data by ID.&#34;&#34;&#34; &#34;&#34;&#34;Load a session&#39;s raw data by ID.&#34;&#34;&#34;
from connpy.ai import ai from connpy.ai import ai
@@ -379,28 +461,69 @@ el.replaceWith(d);
prev_pos = cmd_byte_positions[i-1][0] prev_pos = cmd_byte_positions[i-1][0]
if known_cmd: if known_cmd:
prev_chunk = raw_bytes[prev_pos:pos] if known_cmd == &#34;CANCELLED&#34;:
prev_cleaned = log_cleaner(prev_chunk.decode(errors=&#39;replace&#39;)) parsed_positions.append({&#34;pos&#34;: pos, &#34;type&#34;: &#34;CANCELLED&#34;, &#34;preview&#34;: &#34;&#34;})
prev_lines = [l for l in prev_cleaned.split(&#39;\n&#39;) if l.strip()] else:
prompt_text = prev_lines[-1].strip() if prev_lines else &#34;&#34; prev_chunk = raw_bytes[prev_pos:pos]
preview = f&#34;{prompt_text}{known_cmd}&#34; if prompt_text else known_cmd prev_cleaned = self._clean_cisco_scrolling(prev_chunk.decode(errors=&#39;replace&#39;))
parsed_positions.append({&#34;pos&#34;: pos, &#34;type&#34;: &#34;VALID_CMD&#34;, &#34;preview&#34;: preview[:80]}) prev_lines = [l for l in prev_cleaned.split(&#39;\n&#39;) if l.strip()]
prompt_text = prev_lines[-1].strip() if prev_lines else &#34;&#34;
preview = f&#34;{prompt_text}{known_cmd}&#34; if prompt_text else known_cmd
if len(preview) &gt; 80:
preview = preview[:77] + &#34;...&#34;
parsed_positions.append({&#34;pos&#34;: pos, &#34;type&#34;: &#34;VALID_CMD&#34;, &#34;preview&#34;: preview})
else: else:
chunk = raw_bytes[prev_pos:pos] chunk = raw_bytes[prev_pos:pos]
cleaned = log_cleaner(chunk.decode(errors=&#39;replace&#39;))
lines = [l for l in cleaned.split(&#39;\n&#39;) if l.strip()]
preview = lines[-1].strip() if lines else &#34;&#34;
if preview: cleaned = self._clean_cisco_scrolling(chunk.decode(errors=&#39;replace&#39;))
match = prompt_re.search(preview) lines = [l for l in cleaned.split(&#39;\n&#39;) if l.strip()]
if match:
cmd_text = preview[match.end():].strip() found_in_pass1 = False
if cmd_text: if lines:
parsed_positions.append({&#34;pos&#34;: pos, &#34;type&#34;: &#34;VALID_CMD&#34;, &#34;preview&#34;: preview[:80]}) # Search backwards through the last few lines for the prompt
else: for idx in range(len(lines) - 1, max(-1, len(lines) - 10), -1):
parsed_positions.append({&#34;pos&#34;: pos, &#34;type&#34;: &#34;EMPTY_PROMPT&#34;, &#34;preview&#34;: &#34;&#34;}) match = prompt_re.search(lines[idx])
else: if match:
parsed_positions.append({&#34;pos&#34;: pos, &#34;type&#34;: &#34;SCROLLING&#34;, &#34;preview&#34;: &#34;&#34;}) ptxt = match.group(0).strip()
cmd_first_line = lines[idx][match.end():].strip()
cmd_rest = [l.strip() for l in lines[idx+1:]]
cmd_text = &#34; &#34;.join([cmd_first_line] + cmd_rest).strip()
if cmd_text:
pv = f&#34;{ptxt} {cmd_text}&#34;.strip()
if len(pv) &gt; 80:
pv = pv[:77] + &#34;...&#34;
parsed_positions.append({&#34;pos&#34;: pos, &#34;type&#34;: &#34;VALID_CMD&#34;, &#34;preview&#34;: pv})
else:
parsed_positions.append({&#34;pos&#34;: pos, &#34;type&#34;: &#34;EMPTY_PROMPT&#34;, &#34;preview&#34;: &#34;&#34;})
found_in_pass1 = True
break
if not found_in_pass1:
# Fallback: The prompt might have been isolated in the previous chunk
# due to asynchronous network delays splitting the output exactly at the newline.
prev_was_valid_cmd = i &gt;= 2 and parsed_positions[i-2][&#34;type&#34;] == &#34;VALID_CMD&#34;
if prev_pos &gt; 0 and not prev_was_valid_cmd:
# Fetch the very last chunk that we just processed
prev_prev_pos = cmd_byte_positions[i-2][0] if i &gt;= 2 else 0
prev_chunk_text = self._clean_cisco_scrolling(raw_bytes[prev_prev_pos:prev_pos].decode(errors=&#39;replace&#39;))
prev_lines_text = [l for l in prev_chunk_text.split(&#39;\n&#39;) if l.strip()]
if prev_lines_text:
prev_match = prompt_re.search(prev_lines_text[-1])
if prev_match:
ptxt = prev_match.group(0).strip()
cmd_text = &#34; &#34;.join([l.strip() for l in lines]).strip()
if cmd_text:
pv = f&#34;{ptxt} {cmd_text}&#34;.strip()
if len(pv) &gt; 80:
pv = pv[:77] + &#34;...&#34;
parsed_positions.append({&#34;pos&#34;: pos, &#34;type&#34;: &#34;VALID_CMD&#34;, &#34;preview&#34;: pv})
found_in_pass1 = True
if not found_in_pass1:
parsed_positions.append({&#34;pos&#34;: pos, &#34;type&#34;: &#34;SCROLLING&#34;, &#34;preview&#34;: &#34;&#34;})
else: else:
parsed_positions.append({&#34;pos&#34;: pos, &#34;type&#34;: &#34;SCROLLING&#34;, &#34;preview&#34;: &#34;&#34;}) parsed_positions.append({&#34;pos&#34;: pos, &#34;type&#34;: &#34;SCROLLING&#34;, &#34;preview&#34;: &#34;&#34;})
@@ -413,11 +536,11 @@ el.replaceWith(d);
start_pos = item[&#34;pos&#34;] start_pos = item[&#34;pos&#34;]
preview = item[&#34;preview&#34;] preview = item[&#34;preview&#34;]
# Find the end position: next VALID_CMD or EMPTY_PROMPT # Find the end position: next VALID_CMD or EMPTY_PROMPT or CANCELLED
end_pos = current_prompt_pos end_pos = current_prompt_pos
for j in range(i + 1, len(parsed_positions)): for j in range(i + 1, len(parsed_positions)):
next_item = parsed_positions[j] next_item = parsed_positions[j]
if next_item[&#34;type&#34;] in (&#34;VALID_CMD&#34;, &#34;EMPTY_PROMPT&#34;): if next_item[&#34;type&#34;] in (&#34;VALID_CMD&#34;, &#34;EMPTY_PROMPT&#34;, &#34;CANCELLED&#34;):
end_pos = next_item[&#34;pos&#34;] end_pos = next_item[&#34;pos&#34;]
break break
@@ -478,20 +601,22 @@ el.replaceWith(d);
<div class="desc"><p>Update MCP server settings in the configuration with smart merging.</p></div> <div class="desc"><p>Update MCP server settings in the configuration with smart merging.</p></div>
</dd> </dd>
<dt id="connpy.services.ai_service.AIService.configure_provider"><code class="name flex"> <dt id="connpy.services.ai_service.AIService.configure_provider"><code class="name flex">
<span>def <span class="ident">configure_provider</span></span>(<span>self, provider, model=None, api_key=None)</span> <span>def <span class="ident">configure_provider</span></span>(<span>self, provider, model=None, api_key=None, auth=None)</span>
</code></dt> </code></dt>
<dd> <dd>
<details class="source"> <details class="source">
<summary> <summary>
<span>Expand source code</span> <span>Expand source code</span>
</summary> </summary>
<pre><code class="python">def configure_provider(self, provider, model=None, api_key=None): <pre><code class="python">def configure_provider(self, provider, model=None, api_key=None, auth=None):
&#34;&#34;&#34;Update AI provider settings in the configuration.&#34;&#34;&#34; &#34;&#34;&#34;Update AI provider settings in the configuration.&#34;&#34;&#34;
settings = self.config.config.get(&#34;ai&#34;, {}) settings = self.config.config.get(&#34;ai&#34;, {})
if model: if model:
settings[f&#34;{provider}_model&#34;] = model settings[f&#34;{provider}_model&#34;] = model
if api_key: if api_key:
settings[f&#34;{provider}_api_key&#34;] = api_key settings[f&#34;{provider}_api_key&#34;] = api_key
if auth is not None:
settings[f&#34;{provider}_auth&#34;] = auth
self.config.config[&#34;ai&#34;] = settings self.config.config[&#34;ai&#34;] = settings
self.config._saveconfig(self.config.file)</code></pre> self.config._saveconfig(self.config.file)</code></pre>
@@ -534,21 +659,39 @@ el.replaceWith(d);
</details> </details>
<div class="desc"><p>Delete an AI session by ID.</p></div> <div class="desc"><p>Delete an AI session by ID.</p></div>
</dd> </dd>
<dt id="connpy.services.ai_service.AIService.list_sessions"><code class="name flex"> <dt id="connpy.services.ai_service.AIService.list_mcp_servers"><code class="name flex">
<span>def <span class="ident">list_sessions</span></span>(<span>self)</span> <span>def <span class="ident">list_mcp_servers</span></span>(<span>self) > dict</span>
</code></dt> </code></dt>
<dd> <dd>
<details class="source"> <details class="source">
<summary> <summary>
<span>Expand source code</span> <span>Expand source code</span>
</summary> </summary>
<pre><code class="python">def list_sessions(self): <pre><code class="python">def list_mcp_servers(self) -&gt; dict:
&#34;&#34;&#34;Return a list of all saved AI sessions.&#34;&#34;&#34; &#34;&#34;&#34;Get the configured MCP servers.&#34;&#34;&#34;
ai_settings = self.config.config.get(&#34;ai&#34;, {})
return ai_settings.get(&#34;mcp_servers&#34;, {})</code></pre>
</details>
<div class="desc"><p>Get the configured MCP servers.</p></div>
</dd>
<dt id="connpy.services.ai_service.AIService.list_sessions"><code class="name flex">
<span>def <span class="ident">list_sessions</span></span>(<span>self, limit=None)</span>
</code></dt>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">def list_sessions(self, limit=None):
&#34;&#34;&#34;Return a list of saved AI sessions, optionally limited.&#34;&#34;&#34;
from connpy.ai import ai from connpy.ai import ai
agent = ai(self.config) agent = ai(self.config)
return agent._get_sessions()</code></pre> sessions = agent._get_sessions()
if limit and len(sessions) &gt; limit:
return sessions[:limit], len(sessions)
return sessions, len(sessions)</code></pre>
</details> </details>
<div class="desc"><p>Return a list of all saved AI sessions.</p></div> <div class="desc"><p>Return a list of saved AI sessions, optionally limited.</p></div>
</dd> </dd>
<dt id="connpy.services.ai_service.AIService.load_session_data"><code class="name flex"> <dt id="connpy.services.ai_service.AIService.load_session_data"><code class="name flex">
<span>def <span class="ident">load_session_data</span></span>(<span>self, session_id)</span> <span>def <span class="ident">load_session_data</span></span>(<span>self, session_id)</span>
@@ -671,6 +814,7 @@ el.replaceWith(d);
<li><code><a title="connpy.services.ai_service.AIService.configure_provider" href="#connpy.services.ai_service.AIService.configure_provider">configure_provider</a></code></li> <li><code><a title="connpy.services.ai_service.AIService.configure_provider" href="#connpy.services.ai_service.AIService.configure_provider">configure_provider</a></code></li>
<li><code><a title="connpy.services.ai_service.AIService.confirm" href="#connpy.services.ai_service.AIService.confirm">confirm</a></code></li> <li><code><a title="connpy.services.ai_service.AIService.confirm" href="#connpy.services.ai_service.AIService.confirm">confirm</a></code></li>
<li><code><a title="connpy.services.ai_service.AIService.delete_session" href="#connpy.services.ai_service.AIService.delete_session">delete_session</a></code></li> <li><code><a title="connpy.services.ai_service.AIService.delete_session" href="#connpy.services.ai_service.AIService.delete_session">delete_session</a></code></li>
<li><code><a title="connpy.services.ai_service.AIService.list_mcp_servers" href="#connpy.services.ai_service.AIService.list_mcp_servers">list_mcp_servers</a></code></li>
<li><code><a title="connpy.services.ai_service.AIService.list_sessions" href="#connpy.services.ai_service.AIService.list_sessions">list_sessions</a></code></li> <li><code><a title="connpy.services.ai_service.AIService.list_sessions" href="#connpy.services.ai_service.AIService.list_sessions">list_sessions</a></code></li>
<li><code><a title="connpy.services.ai_service.AIService.load_session_data" href="#connpy.services.ai_service.AIService.load_session_data">load_session_data</a></code></li> <li><code><a title="connpy.services.ai_service.AIService.load_session_data" href="#connpy.services.ai_service.AIService.load_session_data">load_session_data</a></code></li>
<li><code><a title="connpy.services.ai_service.AIService.process_copilot_input" href="#connpy.services.ai_service.AIService.process_copilot_input">process_copilot_input</a></code></li> <li><code><a title="connpy.services.ai_service.AIService.process_copilot_input" href="#connpy.services.ai_service.AIService.process_copilot_input">process_copilot_input</a></code></li>
@@ -682,7 +826,7 @@ el.replaceWith(d);
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.services.base API documentation</title> <title>connpy.services.base API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -152,7 +152,7 @@ el.replaceWith(d);
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.services.config_service API documentation</title> <title>connpy.services.config_service API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -319,7 +319,7 @@ el.replaceWith(d);
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.services.context_service API documentation</title> <title>connpy.services.context_service API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -370,7 +370,7 @@ def current_context(self) -&gt; str:
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.services.exceptions API documentation</title> <title>connpy.services.exceptions API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -268,7 +268,7 @@ el.replaceWith(d);
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.services.execution_service API documentation</title> <title>connpy.services.execution_service API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -449,7 +449,7 @@ el.replaceWith(d);
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.services.import_export_service API documentation</title> <title>connpy.services.import_export_service API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -361,7 +361,7 @@ el.replaceWith(d);
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+200 -56
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.services API documentation</title> <title>connpy.services API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -113,6 +113,37 @@ el.replaceWith(d);
<pre><code class="python">class AIService(BaseService): <pre><code class="python">class AIService(BaseService):
&#34;&#34;&#34;Business logic for interacting with AI agents and LLM configurations.&#34;&#34;&#34; &#34;&#34;&#34;Business logic for interacting with AI agents and LLM configurations.&#34;&#34;&#34;
def _clean_cisco_scrolling(self, text: str) -&gt; str:
&#34;&#34;&#34;Resolves horizontal scrolling artifacts (backspaces, \r, ANSI) by merging overlapping segments.&#34;&#34;&#34;
def merge_overlapping(s1, s2):
s2_clean = s2.lstrip(&#39; $&#39;)
max_overlap = min(len(s1), len(s2_clean))
for i in range(max_overlap, 0, -1):
if s1[-i:] == s2_clean[:i]:
return s1 + s2_clean[i:]
return s1 + s2_clean
scroll_re = re.compile(r&#39;(\x08{5,}\s*\$?|\$\r|\x1b\[\d+[GD]\s*\$?)&#39;)
parts = scroll_re.split(text)
merged = &#34;&#34;
for part in parts:
if scroll_re.match(part):
continue
cleaned = log_cleaner(part)
if not merged:
merged = cleaned
else:
merged_lines = merged.split(&#39;\n&#39;)
cleaned_lines = cleaned.split(&#39;\n&#39;)
merged_lines[-1] = merge_overlapping(merged_lines[-1], cleaned_lines[0])
merged_lines.extend(cleaned_lines[1:])
merged = &#34;\n&#34;.join(merged_lines)
return merged
def build_context_blocks(self, raw_bytes: bytes, cmd_byte_positions: list, node_info: dict, last_line: str = &#34;&#34;) -&gt; list: def build_context_blocks(self, raw_bytes: bytes, cmd_byte_positions: list, node_info: dict, last_line: str = &#34;&#34;) -&gt; list:
&#34;&#34;&#34;Identifies command blocks in the terminal history.&#34;&#34;&#34; &#34;&#34;&#34;Identifies command blocks in the terminal history.&#34;&#34;&#34;
blocks = [] blocks = []
@@ -134,28 +165,69 @@ el.replaceWith(d);
prev_pos = cmd_byte_positions[i-1][0] prev_pos = cmd_byte_positions[i-1][0]
if known_cmd: if known_cmd:
prev_chunk = raw_bytes[prev_pos:pos] if known_cmd == &#34;CANCELLED&#34;:
prev_cleaned = log_cleaner(prev_chunk.decode(errors=&#39;replace&#39;)) parsed_positions.append({&#34;pos&#34;: pos, &#34;type&#34;: &#34;CANCELLED&#34;, &#34;preview&#34;: &#34;&#34;})
prev_lines = [l for l in prev_cleaned.split(&#39;\n&#39;) if l.strip()] else:
prompt_text = prev_lines[-1].strip() if prev_lines else &#34;&#34; prev_chunk = raw_bytes[prev_pos:pos]
preview = f&#34;{prompt_text}{known_cmd}&#34; if prompt_text else known_cmd prev_cleaned = self._clean_cisco_scrolling(prev_chunk.decode(errors=&#39;replace&#39;))
parsed_positions.append({&#34;pos&#34;: pos, &#34;type&#34;: &#34;VALID_CMD&#34;, &#34;preview&#34;: preview[:80]}) prev_lines = [l for l in prev_cleaned.split(&#39;\n&#39;) if l.strip()]
prompt_text = prev_lines[-1].strip() if prev_lines else &#34;&#34;
preview = f&#34;{prompt_text}{known_cmd}&#34; if prompt_text else known_cmd
if len(preview) &gt; 80:
preview = preview[:77] + &#34;...&#34;
parsed_positions.append({&#34;pos&#34;: pos, &#34;type&#34;: &#34;VALID_CMD&#34;, &#34;preview&#34;: preview})
else: else:
chunk = raw_bytes[prev_pos:pos] chunk = raw_bytes[prev_pos:pos]
cleaned = log_cleaner(chunk.decode(errors=&#39;replace&#39;))
lines = [l for l in cleaned.split(&#39;\n&#39;) if l.strip()]
preview = lines[-1].strip() if lines else &#34;&#34;
if preview: cleaned = self._clean_cisco_scrolling(chunk.decode(errors=&#39;replace&#39;))
match = prompt_re.search(preview) lines = [l for l in cleaned.split(&#39;\n&#39;) if l.strip()]
if match:
cmd_text = preview[match.end():].strip() found_in_pass1 = False
if cmd_text: if lines:
parsed_positions.append({&#34;pos&#34;: pos, &#34;type&#34;: &#34;VALID_CMD&#34;, &#34;preview&#34;: preview[:80]}) # Search backwards through the last few lines for the prompt
else: for idx in range(len(lines) - 1, max(-1, len(lines) - 10), -1):
parsed_positions.append({&#34;pos&#34;: pos, &#34;type&#34;: &#34;EMPTY_PROMPT&#34;, &#34;preview&#34;: &#34;&#34;}) match = prompt_re.search(lines[idx])
else: if match:
parsed_positions.append({&#34;pos&#34;: pos, &#34;type&#34;: &#34;SCROLLING&#34;, &#34;preview&#34;: &#34;&#34;}) ptxt = match.group(0).strip()
cmd_first_line = lines[idx][match.end():].strip()
cmd_rest = [l.strip() for l in lines[idx+1:]]
cmd_text = &#34; &#34;.join([cmd_first_line] + cmd_rest).strip()
if cmd_text:
pv = f&#34;{ptxt} {cmd_text}&#34;.strip()
if len(pv) &gt; 80:
pv = pv[:77] + &#34;...&#34;
parsed_positions.append({&#34;pos&#34;: pos, &#34;type&#34;: &#34;VALID_CMD&#34;, &#34;preview&#34;: pv})
else:
parsed_positions.append({&#34;pos&#34;: pos, &#34;type&#34;: &#34;EMPTY_PROMPT&#34;, &#34;preview&#34;: &#34;&#34;})
found_in_pass1 = True
break
if not found_in_pass1:
# Fallback: The prompt might have been isolated in the previous chunk
# due to asynchronous network delays splitting the output exactly at the newline.
prev_was_valid_cmd = i &gt;= 2 and parsed_positions[i-2][&#34;type&#34;] == &#34;VALID_CMD&#34;
if prev_pos &gt; 0 and not prev_was_valid_cmd:
# Fetch the very last chunk that we just processed
prev_prev_pos = cmd_byte_positions[i-2][0] if i &gt;= 2 else 0
prev_chunk_text = self._clean_cisco_scrolling(raw_bytes[prev_prev_pos:prev_pos].decode(errors=&#39;replace&#39;))
prev_lines_text = [l for l in prev_chunk_text.split(&#39;\n&#39;) if l.strip()]
if prev_lines_text:
prev_match = prompt_re.search(prev_lines_text[-1])
if prev_match:
ptxt = prev_match.group(0).strip()
cmd_text = &#34; &#34;.join([l.strip() for l in lines]).strip()
if cmd_text:
pv = f&#34;{ptxt} {cmd_text}&#34;.strip()
if len(pv) &gt; 80:
pv = pv[:77] + &#34;...&#34;
parsed_positions.append({&#34;pos&#34;: pos, &#34;type&#34;: &#34;VALID_CMD&#34;, &#34;preview&#34;: pv})
found_in_pass1 = True
if not found_in_pass1:
parsed_positions.append({&#34;pos&#34;: pos, &#34;type&#34;: &#34;SCROLLING&#34;, &#34;preview&#34;: &#34;&#34;})
else: else:
parsed_positions.append({&#34;pos&#34;: pos, &#34;type&#34;: &#34;SCROLLING&#34;, &#34;preview&#34;: &#34;&#34;}) parsed_positions.append({&#34;pos&#34;: pos, &#34;type&#34;: &#34;SCROLLING&#34;, &#34;preview&#34;: &#34;&#34;})
@@ -168,11 +240,11 @@ el.replaceWith(d);
start_pos = item[&#34;pos&#34;] start_pos = item[&#34;pos&#34;]
preview = item[&#34;preview&#34;] preview = item[&#34;preview&#34;]
# Find the end position: next VALID_CMD or EMPTY_PROMPT # Find the end position: next VALID_CMD or EMPTY_PROMPT or CANCELLED
end_pos = current_prompt_pos end_pos = current_prompt_pos
for j in range(i + 1, len(parsed_positions)): for j in range(i + 1, len(parsed_positions)):
next_item = parsed_positions[j] next_item = parsed_positions[j]
if next_item[&#34;type&#34;] in (&#34;VALID_CMD&#34;, &#34;EMPTY_PROMPT&#34;): if next_item[&#34;type&#34;] in (&#34;VALID_CMD&#34;, &#34;EMPTY_PROMPT&#34;, &#34;CANCELLED&#34;):
end_pos = next_item[&#34;pos&#34;] end_pos = next_item[&#34;pos&#34;]
break break
@@ -274,11 +346,14 @@ el.replaceWith(d);
return await asyncio.wrap_future(future) return await asyncio.wrap_future(future)
def list_sessions(self): def list_sessions(self, limit=None):
&#34;&#34;&#34;Return a list of all saved AI sessions.&#34;&#34;&#34; &#34;&#34;&#34;Return a list of saved AI sessions, optionally limited.&#34;&#34;&#34;
from connpy.ai import ai from connpy.ai import ai
agent = ai(self.config) agent = ai(self.config)
return agent._get_sessions() sessions = agent._get_sessions()
if limit and len(sessions) &gt; limit:
return sessions[:limit], len(sessions)
return sessions, len(sessions)
def delete_session(self, session_id): def delete_session(self, session_id):
&#34;&#34;&#34;Delete an AI session by ID.&#34;&#34;&#34; &#34;&#34;&#34;Delete an AI session by ID.&#34;&#34;&#34;
@@ -290,13 +365,15 @@ el.replaceWith(d);
else: else:
raise InvalidConfigurationError(f&#34;Session &#39;{session_id}&#39; not found.&#34;) raise InvalidConfigurationError(f&#34;Session &#39;{session_id}&#39; not found.&#34;)
def configure_provider(self, provider, model=None, api_key=None): def configure_provider(self, provider, model=None, api_key=None, auth=None):
&#34;&#34;&#34;Update AI provider settings in the configuration.&#34;&#34;&#34; &#34;&#34;&#34;Update AI provider settings in the configuration.&#34;&#34;&#34;
settings = self.config.config.get(&#34;ai&#34;, {}) settings = self.config.config.get(&#34;ai&#34;, {})
if model: if model:
settings[f&#34;{provider}_model&#34;] = model settings[f&#34;{provider}_model&#34;] = model
if api_key: if api_key:
settings[f&#34;{provider}_api_key&#34;] = api_key settings[f&#34;{provider}_api_key&#34;] = api_key
if auth is not None:
settings[f&#34;{provider}_auth&#34;] = auth
self.config.config[&#34;ai&#34;] = settings self.config.config[&#34;ai&#34;] = settings
self.config._saveconfig(self.config.file) self.config._saveconfig(self.config.file)
@@ -335,6 +412,11 @@ el.replaceWith(d);
self.config.config[&#34;ai&#34;] = ai_settings self.config.config[&#34;ai&#34;] = ai_settings
self.config._saveconfig(self.config.file) self.config._saveconfig(self.config.file)
def list_mcp_servers(self) -&gt; dict:
&#34;&#34;&#34;Get the configured MCP servers.&#34;&#34;&#34;
ai_settings = self.config.config.get(&#34;ai&#34;, {})
return ai_settings.get(&#34;mcp_servers&#34;, {})
def load_session_data(self, session_id): def load_session_data(self, session_id):
&#34;&#34;&#34;Load a session&#39;s raw data by ID.&#34;&#34;&#34; &#34;&#34;&#34;Load a session&#39;s raw data by ID.&#34;&#34;&#34;
from connpy.ai import ai from connpy.ai import ai
@@ -434,28 +516,69 @@ el.replaceWith(d);
prev_pos = cmd_byte_positions[i-1][0] prev_pos = cmd_byte_positions[i-1][0]
if known_cmd: if known_cmd:
prev_chunk = raw_bytes[prev_pos:pos] if known_cmd == &#34;CANCELLED&#34;:
prev_cleaned = log_cleaner(prev_chunk.decode(errors=&#39;replace&#39;)) parsed_positions.append({&#34;pos&#34;: pos, &#34;type&#34;: &#34;CANCELLED&#34;, &#34;preview&#34;: &#34;&#34;})
prev_lines = [l for l in prev_cleaned.split(&#39;\n&#39;) if l.strip()] else:
prompt_text = prev_lines[-1].strip() if prev_lines else &#34;&#34; prev_chunk = raw_bytes[prev_pos:pos]
preview = f&#34;{prompt_text}{known_cmd}&#34; if prompt_text else known_cmd prev_cleaned = self._clean_cisco_scrolling(prev_chunk.decode(errors=&#39;replace&#39;))
parsed_positions.append({&#34;pos&#34;: pos, &#34;type&#34;: &#34;VALID_CMD&#34;, &#34;preview&#34;: preview[:80]}) prev_lines = [l for l in prev_cleaned.split(&#39;\n&#39;) if l.strip()]
prompt_text = prev_lines[-1].strip() if prev_lines else &#34;&#34;
preview = f&#34;{prompt_text}{known_cmd}&#34; if prompt_text else known_cmd
if len(preview) &gt; 80:
preview = preview[:77] + &#34;...&#34;
parsed_positions.append({&#34;pos&#34;: pos, &#34;type&#34;: &#34;VALID_CMD&#34;, &#34;preview&#34;: preview})
else: else:
chunk = raw_bytes[prev_pos:pos] chunk = raw_bytes[prev_pos:pos]
cleaned = log_cleaner(chunk.decode(errors=&#39;replace&#39;))
lines = [l for l in cleaned.split(&#39;\n&#39;) if l.strip()]
preview = lines[-1].strip() if lines else &#34;&#34;
if preview: cleaned = self._clean_cisco_scrolling(chunk.decode(errors=&#39;replace&#39;))
match = prompt_re.search(preview) lines = [l for l in cleaned.split(&#39;\n&#39;) if l.strip()]
if match:
cmd_text = preview[match.end():].strip() found_in_pass1 = False
if cmd_text: if lines:
parsed_positions.append({&#34;pos&#34;: pos, &#34;type&#34;: &#34;VALID_CMD&#34;, &#34;preview&#34;: preview[:80]}) # Search backwards through the last few lines for the prompt
else: for idx in range(len(lines) - 1, max(-1, len(lines) - 10), -1):
parsed_positions.append({&#34;pos&#34;: pos, &#34;type&#34;: &#34;EMPTY_PROMPT&#34;, &#34;preview&#34;: &#34;&#34;}) match = prompt_re.search(lines[idx])
else: if match:
parsed_positions.append({&#34;pos&#34;: pos, &#34;type&#34;: &#34;SCROLLING&#34;, &#34;preview&#34;: &#34;&#34;}) ptxt = match.group(0).strip()
cmd_first_line = lines[idx][match.end():].strip()
cmd_rest = [l.strip() for l in lines[idx+1:]]
cmd_text = &#34; &#34;.join([cmd_first_line] + cmd_rest).strip()
if cmd_text:
pv = f&#34;{ptxt} {cmd_text}&#34;.strip()
if len(pv) &gt; 80:
pv = pv[:77] + &#34;...&#34;
parsed_positions.append({&#34;pos&#34;: pos, &#34;type&#34;: &#34;VALID_CMD&#34;, &#34;preview&#34;: pv})
else:
parsed_positions.append({&#34;pos&#34;: pos, &#34;type&#34;: &#34;EMPTY_PROMPT&#34;, &#34;preview&#34;: &#34;&#34;})
found_in_pass1 = True
break
if not found_in_pass1:
# Fallback: The prompt might have been isolated in the previous chunk
# due to asynchronous network delays splitting the output exactly at the newline.
prev_was_valid_cmd = i &gt;= 2 and parsed_positions[i-2][&#34;type&#34;] == &#34;VALID_CMD&#34;
if prev_pos &gt; 0 and not prev_was_valid_cmd:
# Fetch the very last chunk that we just processed
prev_prev_pos = cmd_byte_positions[i-2][0] if i &gt;= 2 else 0
prev_chunk_text = self._clean_cisco_scrolling(raw_bytes[prev_prev_pos:prev_pos].decode(errors=&#39;replace&#39;))
prev_lines_text = [l for l in prev_chunk_text.split(&#39;\n&#39;) if l.strip()]
if prev_lines_text:
prev_match = prompt_re.search(prev_lines_text[-1])
if prev_match:
ptxt = prev_match.group(0).strip()
cmd_text = &#34; &#34;.join([l.strip() for l in lines]).strip()
if cmd_text:
pv = f&#34;{ptxt} {cmd_text}&#34;.strip()
if len(pv) &gt; 80:
pv = pv[:77] + &#34;...&#34;
parsed_positions.append({&#34;pos&#34;: pos, &#34;type&#34;: &#34;VALID_CMD&#34;, &#34;preview&#34;: pv})
found_in_pass1 = True
if not found_in_pass1:
parsed_positions.append({&#34;pos&#34;: pos, &#34;type&#34;: &#34;SCROLLING&#34;, &#34;preview&#34;: &#34;&#34;})
else: else:
parsed_positions.append({&#34;pos&#34;: pos, &#34;type&#34;: &#34;SCROLLING&#34;, &#34;preview&#34;: &#34;&#34;}) parsed_positions.append({&#34;pos&#34;: pos, &#34;type&#34;: &#34;SCROLLING&#34;, &#34;preview&#34;: &#34;&#34;})
@@ -468,11 +591,11 @@ el.replaceWith(d);
start_pos = item[&#34;pos&#34;] start_pos = item[&#34;pos&#34;]
preview = item[&#34;preview&#34;] preview = item[&#34;preview&#34;]
# Find the end position: next VALID_CMD or EMPTY_PROMPT # Find the end position: next VALID_CMD or EMPTY_PROMPT or CANCELLED
end_pos = current_prompt_pos end_pos = current_prompt_pos
for j in range(i + 1, len(parsed_positions)): for j in range(i + 1, len(parsed_positions)):
next_item = parsed_positions[j] next_item = parsed_positions[j]
if next_item[&#34;type&#34;] in (&#34;VALID_CMD&#34;, &#34;EMPTY_PROMPT&#34;): if next_item[&#34;type&#34;] in (&#34;VALID_CMD&#34;, &#34;EMPTY_PROMPT&#34;, &#34;CANCELLED&#34;):
end_pos = next_item[&#34;pos&#34;] end_pos = next_item[&#34;pos&#34;]
break break
@@ -533,20 +656,22 @@ el.replaceWith(d);
<div class="desc"><p>Update MCP server settings in the configuration with smart merging.</p></div> <div class="desc"><p>Update MCP server settings in the configuration with smart merging.</p></div>
</dd> </dd>
<dt id="connpy.services.AIService.configure_provider"><code class="name flex"> <dt id="connpy.services.AIService.configure_provider"><code class="name flex">
<span>def <span class="ident">configure_provider</span></span>(<span>self, provider, model=None, api_key=None)</span> <span>def <span class="ident">configure_provider</span></span>(<span>self, provider, model=None, api_key=None, auth=None)</span>
</code></dt> </code></dt>
<dd> <dd>
<details class="source"> <details class="source">
<summary> <summary>
<span>Expand source code</span> <span>Expand source code</span>
</summary> </summary>
<pre><code class="python">def configure_provider(self, provider, model=None, api_key=None): <pre><code class="python">def configure_provider(self, provider, model=None, api_key=None, auth=None):
&#34;&#34;&#34;Update AI provider settings in the configuration.&#34;&#34;&#34; &#34;&#34;&#34;Update AI provider settings in the configuration.&#34;&#34;&#34;
settings = self.config.config.get(&#34;ai&#34;, {}) settings = self.config.config.get(&#34;ai&#34;, {})
if model: if model:
settings[f&#34;{provider}_model&#34;] = model settings[f&#34;{provider}_model&#34;] = model
if api_key: if api_key:
settings[f&#34;{provider}_api_key&#34;] = api_key settings[f&#34;{provider}_api_key&#34;] = api_key
if auth is not None:
settings[f&#34;{provider}_auth&#34;] = auth
self.config.config[&#34;ai&#34;] = settings self.config.config[&#34;ai&#34;] = settings
self.config._saveconfig(self.config.file)</code></pre> self.config._saveconfig(self.config.file)</code></pre>
@@ -589,21 +714,39 @@ el.replaceWith(d);
</details> </details>
<div class="desc"><p>Delete an AI session by ID.</p></div> <div class="desc"><p>Delete an AI session by ID.</p></div>
</dd> </dd>
<dt id="connpy.services.AIService.list_sessions"><code class="name flex"> <dt id="connpy.services.AIService.list_mcp_servers"><code class="name flex">
<span>def <span class="ident">list_sessions</span></span>(<span>self)</span> <span>def <span class="ident">list_mcp_servers</span></span>(<span>self) > dict</span>
</code></dt> </code></dt>
<dd> <dd>
<details class="source"> <details class="source">
<summary> <summary>
<span>Expand source code</span> <span>Expand source code</span>
</summary> </summary>
<pre><code class="python">def list_sessions(self): <pre><code class="python">def list_mcp_servers(self) -&gt; dict:
&#34;&#34;&#34;Return a list of all saved AI sessions.&#34;&#34;&#34; &#34;&#34;&#34;Get the configured MCP servers.&#34;&#34;&#34;
ai_settings = self.config.config.get(&#34;ai&#34;, {})
return ai_settings.get(&#34;mcp_servers&#34;, {})</code></pre>
</details>
<div class="desc"><p>Get the configured MCP servers.</p></div>
</dd>
<dt id="connpy.services.AIService.list_sessions"><code class="name flex">
<span>def <span class="ident">list_sessions</span></span>(<span>self, limit=None)</span>
</code></dt>
<dd>
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">def list_sessions(self, limit=None):
&#34;&#34;&#34;Return a list of saved AI sessions, optionally limited.&#34;&#34;&#34;
from connpy.ai import ai from connpy.ai import ai
agent = ai(self.config) agent = ai(self.config)
return agent._get_sessions()</code></pre> sessions = agent._get_sessions()
if limit and len(sessions) &gt; limit:
return sessions[:limit], len(sessions)
return sessions, len(sessions)</code></pre>
</details> </details>
<div class="desc"><p>Return a list of all saved AI sessions.</p></div> <div class="desc"><p>Return a list of saved AI sessions, optionally limited.</p></div>
</dd> </dd>
<dt id="connpy.services.AIService.load_session_data"><code class="name flex"> <dt id="connpy.services.AIService.load_session_data"><code class="name flex">
<span>def <span class="ident">load_session_data</span></span>(<span>self, session_id)</span> <span>def <span class="ident">load_session_data</span></span>(<span>self, session_id)</span>
@@ -3726,6 +3869,7 @@ el.replaceWith(d);
<li><code><a title="connpy.services.AIService.configure_provider" href="#connpy.services.AIService.configure_provider">configure_provider</a></code></li> <li><code><a title="connpy.services.AIService.configure_provider" href="#connpy.services.AIService.configure_provider">configure_provider</a></code></li>
<li><code><a title="connpy.services.AIService.confirm" href="#connpy.services.AIService.confirm">confirm</a></code></li> <li><code><a title="connpy.services.AIService.confirm" href="#connpy.services.AIService.confirm">confirm</a></code></li>
<li><code><a title="connpy.services.AIService.delete_session" href="#connpy.services.AIService.delete_session">delete_session</a></code></li> <li><code><a title="connpy.services.AIService.delete_session" href="#connpy.services.AIService.delete_session">delete_session</a></code></li>
<li><code><a title="connpy.services.AIService.list_mcp_servers" href="#connpy.services.AIService.list_mcp_servers">list_mcp_servers</a></code></li>
<li><code><a title="connpy.services.AIService.list_sessions" href="#connpy.services.AIService.list_sessions">list_sessions</a></code></li> <li><code><a title="connpy.services.AIService.list_sessions" href="#connpy.services.AIService.list_sessions">list_sessions</a></code></li>
<li><code><a title="connpy.services.AIService.load_session_data" href="#connpy.services.AIService.load_session_data">load_session_data</a></code></li> <li><code><a title="connpy.services.AIService.load_session_data" href="#connpy.services.AIService.load_session_data">load_session_data</a></code></li>
<li><code><a title="connpy.services.AIService.process_copilot_input" href="#connpy.services.AIService.process_copilot_input">process_copilot_input</a></code></li> <li><code><a title="connpy.services.AIService.process_copilot_input" href="#connpy.services.AIService.process_copilot_input">process_copilot_input</a></code></li>
@@ -3840,7 +3984,7 @@ el.replaceWith(d);
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.services.node_service API documentation</title> <title>connpy.services.node_service API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -786,7 +786,7 @@ el.replaceWith(d);
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.services.plugin_service API documentation</title> <title>connpy.services.plugin_service API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -709,7 +709,7 @@ el.replaceWith(d);
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.services.profile_service API documentation</title> <title>connpy.services.profile_service API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -429,7 +429,7 @@ el.replaceWith(d);
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.services.provider API documentation</title> <title>connpy.services.provider API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -164,7 +164,7 @@ el.replaceWith(d);
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.services.sync_service API documentation</title> <title>connpy.services.sync_service API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -964,7 +964,7 @@ el.replaceWith(d);
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.services.system_service API documentation</title> <title>connpy.services.system_service API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -325,7 +325,7 @@ el.replaceWith(d);
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+2 -2
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.tunnels API documentation</title> <title>connpy.tunnels API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -545,7 +545,7 @@ Bridges the blocking gRPC iterators with the async _async_interact_loop.</p></di
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>
+6 -3
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="generator" content="pdoc3 0.11.5"> <meta name="generator" content="pdoc3 0.11.6">
<title>connpy.utils API documentation</title> <title>connpy.utils API documentation</title>
<meta name="description" content=""> <meta name="description" content="">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/13.0.0/sanitize.min.css" integrity="sha512-y1dtMcuvtTMJc1yPgEqF0ZjQbhnc/bFhyvIyVNb9Zk5mIGtqVaAB1Ttl28su8AvFMOY0EwRbAe+HCLqj6W7/KA==" crossorigin>
@@ -59,11 +59,14 @@ el.replaceWith(d);
if not data: if not data:
return &#34;&#34; return &#34;&#34;
# Remove OSC (Operating System Command) sequences (e.g., set window title \x1b]0;...\x07)
data = re.sub(r&#39;\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)&#39;, &#39;&#39;, data)
lines = data.split(&#39;\n&#39;) lines = data.split(&#39;\n&#39;)
cleaned_lines = [] cleaned_lines = []
# Regex to capture: ANSI sequences, control characters (\r, \b, etc), and plain text chunks # Regex to capture: ANSI sequences, control characters (\r, \b, etc), and plain text chunks
token_re = re.compile(r&#39;(\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/ ]*[@-~])|\r|\b|\x7f|[\x00-\x1F]|[^\x1B\r\b\x7f\x00-\x1F]+)&#39;) token_re = re.compile(r&#39;(\x1B(?:[\x30-\x5A\x5C-\x7E]|\[[0-?]*[ -/ ]*[@-~])|\r|\b|\x7f|[\x00-\x1F]|[^\x1B\r\b\x7f\x00-\x1F]+)&#39;)
for line in lines: for line in lines:
buffer = [] buffer = []
@@ -144,7 +147,7 @@ el.replaceWith(d);
</nav> </nav>
</main> </main>
<footer id="footer"> <footer id="footer">
<p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.5</a>.</p> <p>Generated by <a href="https://pdoc3.github.io/pdoc" title="pdoc: Python API documentation generator"><cite>pdoc</cite> 0.11.6</a>.</p>
</footer> </footer>
</body> </body>
</html> </html>