diff --git a/connpy/ai.py b/connpy/ai.py
index 97cb9d1..bf65cb6 100755
--- a/connpy/ai.py
+++ b/connpy/ai.py
@@ -1211,5 +1211,112 @@ class ai:
"streamed": streamed_response
}
+ @MethodHook
+ def ask_copilot(self, terminal_buffer, user_question, node_info=None):
+ """Single-shot copilot for augmented terminal sessions.
+
+ Args:
+ terminal_buffer: Sanitized terminal screen content (últimas N líneas).
+ user_question: Pregunta del usuario sobre la sesión activa.
+ node_info: Optional dict con metadata del nodo (os, name, etc.)
+
+ Returns:
+ dict: {commands: list[str], guide: str, risk_level: str, error: str|None}
+ """
+ import json
+
+ node_info = node_info or {}
+ os_info = node_info.get("os", "unknown")
+ node_name = node_info.get("name", "unknown")
+
+ system_prompt = f"""Role: TERMINAL COPILOT. You assist a network engineer during a live SSH session.
+Rules:
+1. Answer the user's question directly based on the Terminal Context.
+2. If the user asks you to analyze, parse, or extract data from the Terminal Context, DO IT directly in the 'guide' section (you can use markdown tables or lists). Do NOT just give them a command to do it themselves.
+3. If the user wants to execute an action, provide the required CLI commands in the 'commands' array. If no commands are needed, leave it empty.
+4. ULTRA-CONCISE. Keep your guide to the point.
+5. You MUST call provide_copilot_assistance with your response.
+6. risk_level: "low" for read-only/no commands, "high" for config changes, "destructive" for potentially dangerous ops.
+
+Terminal Context:
+{terminal_buffer}
+
+Device OS: {os_info}
+Node: {node_name}"""
+
+ tools = [{
+ "type": "function",
+ "function": {
+ "name": "provide_copilot_assistance",
+ "description": "Provide terminal copilot assistance with suggested commands and a brief guide.",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "commands": {
+ "type": "array",
+ "items": {"type": "string"},
+ "description": "Ordered list of CLI commands. Each item is one command line."
+ },
+ "guide": {
+ "type": "string",
+ "description": "Brief tactical guide in markdown. 3-4 sentences max."
+ },
+ "risk_level": {
+ "type": "string",
+ "enum": ["low", "high", "destructive"],
+ "description": "Risk level: low=read-only, high=config change, destructive=dangerous."
+ }
+ },
+ "required": ["commands", "guide", "risk_level"]
+ }
+ }
+ }]
+
+ messages = [
+ {"role": "system", "content": system_prompt},
+ {"role": "user", "content": user_question}
+ ]
+
+ try:
+ response = completion(
+ model=self.engineer_model,
+ messages=messages,
+ tools=tools,
+ tool_choice={"type": "function", "function": {"name": "provide_copilot_assistance"}},
+ api_key=self.engineer_key,
+ stream=False
+ )
+
+ message = response.choices[0].message
+ if hasattr(message, "tool_calls") and message.tool_calls:
+ for tool_call in message.tool_calls:
+ if tool_call.function.name == "provide_copilot_assistance":
+ try:
+ args = json.loads(tool_call.function.arguments)
+ return {
+ "commands": args.get("commands", []),
+ "guide": args.get("guide", ""),
+ "risk_level": args.get("risk_level", "low"),
+ "error": None
+ }
+ except json.JSONDecodeError:
+ pass
+
+ # Fallback if no tool called or decode error
+ return {
+ "commands": [],
+ "guide": getattr(message, "content", "") or "Could not parse response.",
+ "risk_level": "low",
+ "error": None
+ }
+
+ except Exception as e:
+ return {
+ "commands": [],
+ "guide": "",
+ "risk_level": "low",
+ "error": str(e)
+ }
+
@MethodHook
def confirm(self, user_input): return True
diff --git a/connpy/core.py b/connpy/core.py
index ba00ee9..30c8b9f 100755
--- a/connpy/core.py
+++ b/connpy/core.py
@@ -75,6 +75,7 @@ class node:
- jumphost (str): Reference another node to be used as a jumphost
'''
+ self.config = config
if config == '':
self.idletime = 0
self.key = None
@@ -356,7 +357,7 @@ class node:
with open(self.logfile, "w") as f:
f.write(self._logclean(self.mylog.getvalue().decode(), True))
- async def _async_interact_loop(self, local_stream, resize_callback):
+ async def _async_interact_loop(self, local_stream, resize_callback, copilot_handler=None):
local_stream.setup(resize_callback=resize_callback)
try:
child_fd = self.child.child_fd
@@ -411,11 +412,33 @@ class node:
data = await local_stream.read()
if not data:
break
- try:
- os.write(child_fd, data)
- except OSError:
- break
- self.lastinput = time()
+
+ # Copilot interception
+ if copilot_handler and b'\x00' in data:
+ # Extract clean buffer from session log
+ buffer = ""
+ if hasattr(self, 'mylog'):
+ raw = self.mylog.getvalue().decode(errors='replace')
+ buffer = self._logclean(raw, var=True)
+ # Pass the full buffer to the handler so the user can adjust context size interactively
+
+ # Build node info from available metadata
+ node_info = {"name": getattr(self, 'unique', 'unknown'), "host": getattr(self, 'host', 'unknown')}
+ if isinstance(getattr(self, 'tags', None), dict):
+ node_info["os"] = self.tags.get("os", "unknown")
+
+ # Invoke copilot (async callback handles UI)
+ await copilot_handler(buffer, node_info, local_stream, child_fd)
+ continue
+
+ # Remove any stray \x00 bytes and forward normally
+ clean_data = data.replace(b'\x00', b'')
+ if clean_data:
+ try:
+ os.write(child_fd, clean_data)
+ except OSError:
+ break
+ self.lastinput = time()
async def egress_task():
# Continue stripping newlines from the live stream until we hit real text
@@ -505,7 +528,10 @@ class node:
except Exception:
pass
- asyncio.run(self._async_interact_loop(local_stream, resize_callback))
+ # Build local copilot handler
+ copilot_handler = self._build_local_copilot_handler()
+
+ asyncio.run(self._async_interact_loop(local_stream, resize_callback, copilot_handler=copilot_handler))
finally:
self._teardown_interact_environment()
else:
@@ -515,6 +541,275 @@ class node:
printer.error(f"Connection failed: {str(connect)}")
sys.exit(1)
+ def _build_local_copilot_handler(self):
+ """Build copilot handler for local CLI sessions using rich for rendering."""
+ config = getattr(self, 'config', None) if hasattr(self, 'config') else None
+ if not config:
+ return None
+
+ # Persistent history across copilot invocations within the same session
+ from prompt_toolkit.history import InMemoryHistory
+ copilot_history = InMemoryHistory()
+
+ async def handler(buffer, node_info, stream, child_fd):
+ import termios, tty
+ import asyncio
+ import os
+ import sys
+
+ try:
+ # Disable LocalStream reader so it doesn't steal keystrokes from Prompt
+ loop = asyncio.get_running_loop()
+ loop.remove_reader(sys.stdin.fileno())
+
+ # Override SIGINT so asyncio doesn't kill the event loop when we press Ctrl+C
+ import signal
+ orig_sigint = signal.getsignal(signal.SIGINT)
+ def custom_sigint(sig, frame):
+ raise KeyboardInterrupt()
+ signal.signal(signal.SIGINT, custom_sigint)
+
+ # 1. Salir de raw mode para poder usar input() y rich
+ stdin_fd = sys.stdin.fileno()
+
+ # Get true original settings saved before entering raw mode
+ original_settings = getattr(stream, 'original_tty_settings', None)
+ if original_settings:
+ import copy
+ new_settings = copy.deepcopy(original_settings)
+ new_settings[3] = new_settings[3] & ~termios.ECHOCTL
+ termios.tcsetattr(stdin_fd, termios.TCSADRAIN, new_settings)
+
+ # Remove O_NONBLOCK from stdin so Prompt.ask() works
+ import fcntl
+ flags = fcntl.fcntl(stdin_fd, fcntl.F_GETFL)
+ fcntl.fcntl(stdin_fd, fcntl.F_SETFL, flags & ~os.O_NONBLOCK)
+
+ # Force a carriage return so the UI doesn't start mid-line
+ sys.stdout.write('\r\n')
+ sys.stdout.flush()
+
+ from rich.console import Console
+ from rich.panel import Panel
+ from rich.markdown import Markdown
+ from rich.prompt import Prompt
+ from .printer import connpy_theme
+
+
+ console = Console(theme=connpy_theme)
+ console.print("\n")
+ console.print(Panel(
+ "[bold cyan]AI Terminal Copilot[/bold cyan]\n"
+ "[dim]Type your question. Enter to send, Escape/Ctrl+C to cancel.\n"
+ "Ctrl+\u2191/\u2193 to adjust context lines. \u2191\u2193 for question history.[/dim]",
+ border_style="cyan"
+ ))
+
+ # 2. Capturar pregunta del usuario
+ total_lines = len(buffer.split('\n'))
+ context_lines = [min(50, total_lines)]
+ cancelled = [False]
+
+ from prompt_toolkit import PromptSession
+ from prompt_toolkit.key_binding import KeyBindings
+ from prompt_toolkit.formatted_text import HTML
+
+ bindings = KeyBindings()
+
+ @bindings.add('c-up')
+ def _(event):
+ if context_lines[0] >= total_lines:
+ context_lines[0] = min(50, total_lines)
+ else:
+ context_lines[0] = min(context_lines[0] + 50, total_lines)
+ event.app.invalidate()
+
+ @bindings.add('c-down')
+ def _(event):
+ if context_lines[0] <= min(50, total_lines):
+ context_lines[0] = total_lines
+ else:
+ context_lines[0] = max(context_lines[0] - 50, min(50, total_lines))
+ event.app.invalidate()
+
+ @bindings.add('escape')
+ def _(event):
+ cancelled[0] = True
+ event.app.exit(result='')
+
+ def get_prompt_text():
+ return HTML(f"Ask [Ctx: {context_lines[0]}/{total_lines}L]: ")
+
+ session = PromptSession(history=copilot_history)
+ question = await session.prompt_async(get_prompt_text, key_bindings=bindings)
+ if cancelled[0] or not question.strip():
+ console.print("\n[dim]Copilot cancelled.[/dim]")
+ os.write(child_fd, b'\x15\r')
+ return
+
+ # Slice the buffer dynamically based on selected context
+ buffer_lines = buffer.split('\n')
+ active_buffer = '\n'.join(buffer_lines[-context_lines[0]:])
+
+ # 3. Llamar al AI con spinner
+ from .services.ai_service import AIService
+ service = AIService(config)
+
+ past_questions = copilot_history.get_strings()
+ if len(past_questions) > 1:
+ # Limit history to last 5 questions to save tokens, excluding current
+ recent_history = past_questions[-6:-1]
+ history_text = "\n".join(f"- {q}" for q in recent_history)
+ enriched_question = f"Previous questions in this session:\n{history_text}\n\nCurrent Question:\n{question}"
+ else:
+ enriched_question = question
+
+ with console.status("[bold cyan]Thinking...[/bold cyan]", spinner="dots"):
+ result = await asyncio.to_thread(service.ask_copilot, active_buffer, enriched_question, node_info)
+
+ if result.get("error"):
+ console.print(f"[red]Error: {result['error']}[/red]")
+ return
+
+ # 4. Renderizar respuesta
+ if result.get("guide"):
+ console.print(Panel(
+ Markdown(result["guide"]),
+ title="[bold cyan]Copilot Guide[/bold cyan]",
+ border_style="cyan"
+ ))
+
+ commands = result.get("commands", [])
+ risk = result.get("risk_level", "low")
+ risk_style = {"low": "green", "high": "yellow", "destructive": "red"}.get(risk, "green")
+
+ if commands:
+ cmd_text = "\n".join(f" {i+1}. {cmd}" for i, cmd in enumerate(commands))
+ console.print(Panel(
+ cmd_text,
+ title=f"[bold {risk_style}]Suggested Commands [{risk.upper()}][/bold {risk_style}]",
+ border_style=risk_style
+ ))
+
+ # 5. Preguntar si inyectar (usando prompt_toolkit)
+ confirm_session = PromptSession()
+ confirm_bindings = KeyBindings()
+
+ @confirm_bindings.add('escape')
+ def _(event):
+ event.app.exit(result='n')
+
+ pt_color = "ansi" + risk_style
+ action = await confirm_session.prompt_async(
+ HTML(f"<{pt_color}>Send commands? (y/n/e/number/range) [n]: {pt_color}>"),
+ key_bindings=confirm_bindings
+ )
+
+ if not action.strip():
+ action = "n"
+
+ console.print("[dim]Returning to session...[/dim]\n")
+
+ action_l = action.lower().strip()
+ if action_l in ('y', 'yes', 'all'):
+ os.write(child_fd, b'\x15') # Ctrl+U to clear line
+ await asyncio.sleep(0.1)
+ for cmd in commands:
+ os.write(child_fd, (cmd + "\n").encode())
+ await asyncio.sleep(0.3)
+ elif action_l.startswith('e'):
+ # Edit mode
+ edit_session = PromptSession()
+ cmds_to_edit = []
+
+ if len(action_l) > 1 and action_l[1:].isdigit():
+ idx = int(action_l[1:]) - 1
+ if 0 <= idx < len(commands):
+ cmds_to_edit = [commands[idx]]
+ else:
+ cmds_to_edit = commands
+
+ if cmds_to_edit:
+ target_cmd = "\n".join(cmds_to_edit)
+ try:
+ edited_cmd = await edit_session.prompt_async(
+ HTML("Edit commands (Alt+Enter or Esc,Enter to submit):\n"),
+ default=target_cmd,
+ multiline=True
+ )
+ if edited_cmd.strip():
+ os.write(child_fd, b'\x15')
+ await asyncio.sleep(0.1)
+ for cmd in edited_cmd.split('\n'):
+ if cmd.strip():
+ os.write(child_fd, (cmd.strip() + "\n").encode())
+ await asyncio.sleep(0.3)
+ else:
+ os.write(child_fd, b'\x15\r')
+ except KeyboardInterrupt:
+ os.write(child_fd, b'\x15\r')
+ else:
+ os.write(child_fd, b'\x15\r')
+ elif action_l not in ('n', 'no', ''):
+ try:
+ selected_indices = set()
+ for part in action_l.split(','):
+ part = part.strip()
+ if not part: continue
+ if '-' in part:
+ start_str, end_str = part.split('-', 1)
+ start = int(start_str) - 1
+ end = int(end_str) - 1
+ for i in range(start, end + 1):
+ selected_indices.add(i)
+ else:
+ selected_indices.add(int(part) - 1)
+
+ valid_indices = sorted([i for i in selected_indices if 0 <= i < len(commands)])
+ if valid_indices:
+ os.write(child_fd, b'\x15') # Ctrl+U to clear line
+ await asyncio.sleep(0.1)
+ if len(valid_indices) == 1:
+ os.write(child_fd, (commands[valid_indices[0]] + "\n").encode())
+ else:
+ for idx in valid_indices:
+ os.write(child_fd, (commands[idx] + "\n").encode())
+ await asyncio.sleep(0.3)
+ else:
+ os.write(child_fd, b'\x15\r') # Ctrl+U + Enter to abort line and get new prompt
+ except ValueError:
+ os.write(child_fd, b'\x15\r')
+ else:
+ os.write(child_fd, b'\x15\r')
+ else:
+ console.print("[dim]Returning to session...[/dim]\n")
+ os.write(child_fd, b'\x15\r')
+ except KeyboardInterrupt:
+ if 'console' in locals():
+ console.print("\n[dim]Copilot cancelled via Ctrl+C.[/dim]\n")
+ else:
+ print("\n[dim]Copilot cancelled via Ctrl+C.[/dim]\n")
+ os.write(child_fd, b'\x15\r')
+ except Exception as e:
+ import traceback
+ print(f"\n[ERROR in Copilot Handler] {e}", flush=True)
+ traceback.print_exc()
+ finally:
+ # 6. Restaurar raw mode, O_NONBLOCK y SIGINT
+ tty.setraw(stdin_fd)
+ fcntl.fcntl(stdin_fd, fcntl.F_SETFL, flags | os.O_NONBLOCK)
+ if 'orig_sigint' in locals():
+ signal.signal(signal.SIGINT, orig_sigint)
+
+ # Re-enable LocalStream reader
+ try:
+ loop = asyncio.get_running_loop()
+ loop.add_reader(stdin_fd, stream._read_ready)
+ except Exception:
+ pass
+
+ return handler
+
@MethodHook
def run(self, commands, vars = None,*, folder = '', prompt = r'>$|#$|\$$|>.$|#.$|\$.$', stdout = False, timeout = 10, logger = None):
diff --git a/connpy/grpc_layer/connpy_pb2.py b/connpy/grpc_layer/connpy_pb2.py
index 3cb95c2..1dbfb06 100644
--- a/connpy/grpc_layer/connpy_pb2.py
+++ b/connpy/grpc_layer/connpy_pb2.py
@@ -26,7 +26,7 @@ from google.protobuf import struct_pb2 as google_dot_protobuf_dot_struct__pb2
from google.protobuf import empty_pb2 as google_dot_protobuf_dot_empty__pb2
-DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0c\x63onnpy.proto\x12\x06\x63onnpy\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1bgoogle/protobuf/empty.proto\"\x8a\x01\n\x0fInteractRequest\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0c\n\x04sftp\x18\x02 \x01(\x08\x12\r\n\x05\x64\x65\x62ug\x18\x03 \x01(\x08\x12\x12\n\nstdin_data\x18\x04 \x01(\x0c\x12\x0c\n\x04\x63ols\x18\x05 \x01(\x05\x12\x0c\n\x04rows\x18\x06 \x01(\x05\x12\x1e\n\x16\x63onnection_params_json\x18\x07 \x01(\t\"O\n\x10InteractResponse\x12\x13\n\x0bstdout_data\x18\x01 \x01(\x0c\x12\x0f\n\x07success\x18\x02 \x01(\x08\x12\x15\n\rerror_message\x18\x03 \x01(\t\"7\n\rFilterRequest\x12\x12\n\nfilter_str\x18\x01 \x01(\t\x12\x12\n\nformat_str\x18\x02 \x01(\t\"5\n\rValueResponse\x12$\n\x04\x64\x61ta\x18\x01 \x01(\x0b\x32\x16.google.protobuf.Value\"\x17\n\tIdRequest\x12\n\n\x02id\x18\x01 \x01(\t\"S\n\x0bNodeRequest\x12\n\n\x02id\x18\x01 \x01(\t\x12%\n\x04\x64\x61ta\x18\x02 \x01(\x0b\x32\x17.google.protobuf.Struct\x12\x11\n\tis_folder\x18\x03 \x01(\x08\".\n\rDeleteRequest\x12\n\n\x02id\x18\x01 \x01(\t\x12\x11\n\tis_folder\x18\x02 \x01(\x08\"\x1d\n\x0cMessageValue\x12\r\n\x05value\x18\x01 \x01(\t\";\n\x0bMoveRequest\x12\x0e\n\x06src_id\x18\x01 \x01(\t\x12\x0e\n\x06\x64st_id\x18\x02 \x01(\t\x12\x0c\n\x04\x63opy\x18\x03 \x01(\x08\"W\n\x0b\x42ulkRequest\x12\x0b\n\x03ids\x18\x01 \x03(\t\x12\r\n\x05hosts\x18\x02 \x03(\t\x12,\n\x0b\x63ommon_data\x18\x03 \x01(\x0b\x32\x17.google.protobuf.Struct\"7\n\x0eStructResponse\x12%\n\x04\x64\x61ta\x18\x01 \x01(\x0b\x32\x17.google.protobuf.Struct\"/\n\x0eProfileRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07resolve\x18\x02 \x01(\x08\"6\n\rStructRequest\x12%\n\x04\x64\x61ta\x18\x01 \x01(\x0b\x32\x17.google.protobuf.Struct\"\x1e\n\rStringRequest\x12\r\n\x05value\x18\x01 \x01(\t\"\x1f\n\x0eStringResponse\x12\r\n\x05value\x18\x01 \x01(\t\"C\n\rUpdateRequest\x12\x0b\n\x03key\x18\x01 \x01(\t\x12%\n\x05value\x18\x02 \x01(\x0b\x32\x16.google.protobuf.Value\"B\n\rPluginRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x13\n\x0bsource_file\x18\x02 \x01(\t\x12\x0e\n\x06update\x18\x03 \x01(\x08\"\xa5\x01\n\nRunRequest\x12\r\n\x05nodes\x18\x01 \x03(\t\x12\x10\n\x08\x63ommands\x18\x02 \x03(\t\x12\x0e\n\x06\x66older\x18\x03 \x01(\t\x12\x0e\n\x06prompt\x18\x04 \x01(\t\x12\x10\n\x08parallel\x18\x05 \x01(\x05\x12%\n\x04vars\x18\x06 \x01(\x0b\x32\x17.google.protobuf.Struct\x12\x0f\n\x07timeout\x18\x07 \x01(\x05\x12\x0c\n\x04name\x18\x08 \x01(\t\"\xb8\x01\n\x0bTestRequest\x12\r\n\x05nodes\x18\x01 \x03(\t\x12\x10\n\x08\x63ommands\x18\x02 \x03(\t\x12\x10\n\x08\x65xpected\x18\x03 \x03(\t\x12\x0e\n\x06\x66older\x18\x04 \x01(\t\x12\x0e\n\x06prompt\x18\x05 \x01(\t\x12\x10\n\x08parallel\x18\x06 \x01(\x05\x12%\n\x04vars\x18\x07 \x01(\x0b\x32\x17.google.protobuf.Struct\x12\x0f\n\x07timeout\x18\x08 \x01(\x05\x12\x0c\n\x04name\x18\t \x01(\t\"A\n\rScriptRequest\x12\x0e\n\x06param1\x18\x01 \x01(\t\x12\x0e\n\x06param2\x18\x02 \x01(\t\x12\x10\n\x08parallel\x18\x03 \x01(\x05\"3\n\rExportRequest\x12\x11\n\tfile_path\x18\x01 \x01(\t\x12\x0f\n\x07\x66olders\x18\x02 \x03(\t\"\x1c\n\x0bListRequest\x12\r\n\x05items\x18\x01 \x03(\t\"\xa6\x02\n\nAskRequest\x12\x12\n\ninput_text\x18\x01 \x01(\t\x12\x0e\n\x06\x64ryrun\x18\x02 \x01(\x08\x12,\n\x0c\x63hat_history\x18\x03 \x01(\x0b\x32\x16.google.protobuf.Value\x12\x12\n\nsession_id\x18\x04 \x01(\t\x12\r\n\x05\x64\x65\x62ug\x18\x05 \x01(\x08\x12\x16\n\x0e\x65ngineer_model\x18\x06 \x01(\t\x12\x18\n\x10\x65ngineer_api_key\x18\x07 \x01(\t\x12\x17\n\x0f\x61rchitect_model\x18\x08 \x01(\t\x12\x19\n\x11\x61rchitect_api_key\x18\t \x01(\t\x12\r\n\x05trust\x18\n \x01(\x08\x12\x1b\n\x13\x63onfirmation_answer\x18\x0b \x01(\t\x12\x11\n\tinterrupt\x18\x0c \x01(\x08\"\xc8\x01\n\nAIResponse\x12\x12\n\ntext_chunk\x18\x01 \x01(\t\x12\x10\n\x08is_final\x18\x02 \x01(\x08\x12,\n\x0b\x66ull_result\x18\x03 \x01(\x0b\x32\x17.google.protobuf.Struct\x12\x15\n\rstatus_update\x18\x04 \x01(\t\x12\x15\n\rdebug_message\x18\x05 \x01(\t\x12\x1d\n\x15requires_confirmation\x18\x06 \x01(\x08\x12\x19\n\x11important_message\x18\x07 \x01(\t\"\x1d\n\x0c\x42oolResponse\x12\r\n\x05value\x18\x01 \x01(\x08\"C\n\x0fProviderRequest\x12\x10\n\x08provider\x18\x01 \x01(\t\x12\r\n\x05model\x18\x02 \x01(\t\x12\x0f\n\x07\x61pi_key\x18\x03 \x01(\t\"\x1b\n\nIntRequest\x12\r\n\x05value\x18\x01 \x01(\x05\"p\n\rNodeRunResult\x12\x11\n\tunique_id\x18\x01 \x01(\t\x12\x0e\n\x06output\x18\x02 \x01(\t\x12\x0e\n\x06status\x18\x03 \x01(\x05\x12,\n\x0btest_result\x18\x04 \x01(\x0b\x32\x17.google.protobuf.Struct\"m\n\x12\x46ullReplaceRequest\x12,\n\x0b\x63onnections\x18\x01 \x01(\x0b\x32\x17.google.protobuf.Struct\x12)\n\x08profiles\x18\x02 \x01(\x0b\x32\x17.google.protobuf.Struct2\xe1\x07\n\x0bNodeService\x12<\n\nlist_nodes\x12\x15.connpy.FilterRequest\x1a\x15.connpy.ValueResponse\"\x00\x12>\n\x0clist_folders\x12\x15.connpy.FilterRequest\x1a\x15.connpy.ValueResponse\"\x00\x12?\n\x10get_node_details\x12\x11.connpy.IdRequest\x1a\x16.connpy.StructResponse\"\x00\x12<\n\x0e\x65xplode_unique\x12\x11.connpy.IdRequest\x1a\x15.connpy.ValueResponse\"\x00\x12\x42\n\x0egenerate_cache\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\"\x00\x12\x39\n\x08\x61\x64\x64_node\x12\x13.connpy.NodeRequest\x1a\x16.google.protobuf.Empty\"\x00\x12<\n\x0bupdate_node\x12\x13.connpy.NodeRequest\x1a\x16.google.protobuf.Empty\"\x00\x12>\n\x0b\x64\x65lete_node\x12\x15.connpy.DeleteRequest\x1a\x16.google.protobuf.Empty\"\x00\x12:\n\tmove_node\x12\x13.connpy.MoveRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x39\n\x08\x62ulk_add\x12\x13.connpy.BulkRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x45\n\x16validate_parent_folder\x12\x11.connpy.IdRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x43\n\x12set_reserved_names\x12\x13.connpy.ListRequest\x1a\x16.google.protobuf.Empty\"\x00\x12H\n\rinteract_node\x12\x17.connpy.InteractRequest\x1a\x18.connpy.InteractResponse\"\x00(\x01\x30\x01\x12\x44\n\x0c\x66ull_replace\x12\x1a.connpy.FullReplaceRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x45\n\rget_inventory\x12\x16.google.protobuf.Empty\x1a\x1a.connpy.FullReplaceRequest\"\x00\x32\x96\x03\n\x0eProfileService\x12?\n\rlist_profiles\x12\x15.connpy.FilterRequest\x1a\x15.connpy.ValueResponse\"\x00\x12?\n\x0bget_profile\x12\x16.connpy.ProfileRequest\x1a\x16.connpy.StructResponse\"\x00\x12<\n\x0b\x61\x64\x64_profile\x12\x13.connpy.NodeRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x44\n\x11resolve_node_data\x12\x15.connpy.StructRequest\x1a\x16.connpy.StructResponse\"\x00\x12=\n\x0e\x64\x65lete_profile\x12\x11.connpy.IdRequest\x1a\x16.google.protobuf.Empty\"\x00\x12?\n\x0eupdate_profile\x12\x13.connpy.NodeRequest\x1a\x16.google.protobuf.Empty\"\x00\x32\xae\x03\n\rConfigService\x12@\n\x0cget_settings\x12\x16.google.protobuf.Empty\x1a\x16.connpy.StructResponse\"\x00\x12\x43\n\x0fget_default_dir\x12\x16.google.protobuf.Empty\x1a\x16.connpy.StringResponse\"\x00\x12\x44\n\x11set_config_folder\x12\x15.connpy.StringRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x41\n\x0eupdate_setting\x12\x15.connpy.UpdateRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x43\n\x10\x65ncrypt_password\x12\x15.connpy.StringRequest\x1a\x16.connpy.StringResponse\"\x00\x12H\n\x15\x61pply_theme_from_file\x12\x15.connpy.StringRequest\x1a\x16.connpy.StructResponse\"\x00\x32\xca\x02\n\rPluginService\x12?\n\x0clist_plugins\x12\x16.google.protobuf.Empty\x1a\x15.connpy.ValueResponse\"\x00\x12=\n\nadd_plugin\x12\x15.connpy.PluginRequest\x1a\x16.google.protobuf.Empty\"\x00\x12<\n\rdelete_plugin\x12\x11.connpy.IdRequest\x1a\x16.google.protobuf.Empty\"\x00\x12<\n\renable_plugin\x12\x11.connpy.IdRequest\x1a\x16.google.protobuf.Empty\"\x00\x12=\n\x0e\x64isable_plugin\x12\x11.connpy.IdRequest\x1a\x16.google.protobuf.Empty\"\x00\x32\x9b\x02\n\x10\x45xecutionService\x12=\n\x0crun_commands\x12\x12.connpy.RunRequest\x1a\x15.connpy.NodeRunResult\"\x00\x30\x01\x12?\n\rtest_commands\x12\x13.connpy.TestRequest\x1a\x15.connpy.NodeRunResult\"\x00\x30\x01\x12\x41\n\x0erun_cli_script\x12\x15.connpy.ScriptRequest\x1a\x16.connpy.StructResponse\"\x00\x12\x44\n\x11run_yaml_playbook\x12\x15.connpy.ScriptRequest\x1a\x16.connpy.StructResponse\"\x00\x32\xe2\x01\n\x13ImportExportService\x12\x41\n\x0e\x65xport_to_file\x12\x15.connpy.ExportRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x43\n\x10import_from_file\x12\x15.connpy.StringRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x43\n\x12set_reserved_names\x12\x13.connpy.ListRequest\x1a\x16.google.protobuf.Empty\"\x00\x32\x8e\x03\n\tAIService\x12\x33\n\x03\x61sk\x12\x12.connpy.AskRequest\x1a\x12.connpy.AIResponse\"\x00(\x01\x30\x01\x12\x38\n\x07\x63onfirm\x12\x15.connpy.StringRequest\x1a\x14.connpy.BoolResponse\"\x00\x12@\n\rlist_sessions\x12\x16.google.protobuf.Empty\x1a\x15.connpy.ValueResponse\"\x00\x12\x41\n\x0e\x64\x65lete_session\x12\x15.connpy.StringRequest\x1a\x16.google.protobuf.Empty\"\x00\x12G\n\x12\x63onfigure_provider\x12\x17.connpy.ProviderRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x44\n\x11load_session_data\x12\x15.connpy.StringRequest\x1a\x16.connpy.StructResponse\"\x00\x32\xc2\x02\n\rSystemService\x12\x39\n\tstart_api\x12\x12.connpy.IntRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x39\n\tdebug_api\x12\x12.connpy.IntRequest\x1a\x16.google.protobuf.Empty\"\x00\x12<\n\x08stop_api\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\"\x00\x12;\n\x0brestart_api\x12\x12.connpy.IntRequest\x1a\x16.google.protobuf.Empty\"\x00\x12@\n\x0eget_api_status\x12\x16.google.protobuf.Empty\x1a\x14.connpy.BoolResponse\"\x00\x62\x06proto3')
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0c\x63onnpy.proto\x12\x06\x63onnpy\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1bgoogle/protobuf/empty.proto\"\x8a\x01\n\x0fInteractRequest\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0c\n\x04sftp\x18\x02 \x01(\x08\x12\r\n\x05\x64\x65\x62ug\x18\x03 \x01(\x08\x12\x12\n\nstdin_data\x18\x04 \x01(\x0c\x12\x0c\n\x04\x63ols\x18\x05 \x01(\x05\x12\x0c\n\x04rows\x18\x06 \x01(\x05\x12\x1e\n\x16\x63onnection_params_json\x18\x07 \x01(\t\"O\n\x10InteractResponse\x12\x13\n\x0bstdout_data\x18\x01 \x01(\x0c\x12\x0f\n\x07success\x18\x02 \x01(\x08\x12\x15\n\rerror_message\x18\x03 \x01(\t\"7\n\rFilterRequest\x12\x12\n\nfilter_str\x18\x01 \x01(\t\x12\x12\n\nformat_str\x18\x02 \x01(\t\"5\n\rValueResponse\x12$\n\x04\x64\x61ta\x18\x01 \x01(\x0b\x32\x16.google.protobuf.Value\"\x17\n\tIdRequest\x12\n\n\x02id\x18\x01 \x01(\t\"S\n\x0bNodeRequest\x12\n\n\x02id\x18\x01 \x01(\t\x12%\n\x04\x64\x61ta\x18\x02 \x01(\x0b\x32\x17.google.protobuf.Struct\x12\x11\n\tis_folder\x18\x03 \x01(\x08\".\n\rDeleteRequest\x12\n\n\x02id\x18\x01 \x01(\t\x12\x11\n\tis_folder\x18\x02 \x01(\x08\"\x1d\n\x0cMessageValue\x12\r\n\x05value\x18\x01 \x01(\t\";\n\x0bMoveRequest\x12\x0e\n\x06src_id\x18\x01 \x01(\t\x12\x0e\n\x06\x64st_id\x18\x02 \x01(\t\x12\x0c\n\x04\x63opy\x18\x03 \x01(\x08\"W\n\x0b\x42ulkRequest\x12\x0b\n\x03ids\x18\x01 \x03(\t\x12\r\n\x05hosts\x18\x02 \x03(\t\x12,\n\x0b\x63ommon_data\x18\x03 \x01(\x0b\x32\x17.google.protobuf.Struct\"7\n\x0eStructResponse\x12%\n\x04\x64\x61ta\x18\x01 \x01(\x0b\x32\x17.google.protobuf.Struct\"/\n\x0eProfileRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07resolve\x18\x02 \x01(\x08\"6\n\rStructRequest\x12%\n\x04\x64\x61ta\x18\x01 \x01(\x0b\x32\x17.google.protobuf.Struct\"\x1e\n\rStringRequest\x12\r\n\x05value\x18\x01 \x01(\t\"\x1f\n\x0eStringResponse\x12\r\n\x05value\x18\x01 \x01(\t\"C\n\rUpdateRequest\x12\x0b\n\x03key\x18\x01 \x01(\t\x12%\n\x05value\x18\x02 \x01(\x0b\x32\x16.google.protobuf.Value\"B\n\rPluginRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x13\n\x0bsource_file\x18\x02 \x01(\t\x12\x0e\n\x06update\x18\x03 \x01(\x08\"\xa5\x01\n\nRunRequest\x12\r\n\x05nodes\x18\x01 \x03(\t\x12\x10\n\x08\x63ommands\x18\x02 \x03(\t\x12\x0e\n\x06\x66older\x18\x03 \x01(\t\x12\x0e\n\x06prompt\x18\x04 \x01(\t\x12\x10\n\x08parallel\x18\x05 \x01(\x05\x12%\n\x04vars\x18\x06 \x01(\x0b\x32\x17.google.protobuf.Struct\x12\x0f\n\x07timeout\x18\x07 \x01(\x05\x12\x0c\n\x04name\x18\x08 \x01(\t\"\xb8\x01\n\x0bTestRequest\x12\r\n\x05nodes\x18\x01 \x03(\t\x12\x10\n\x08\x63ommands\x18\x02 \x03(\t\x12\x10\n\x08\x65xpected\x18\x03 \x03(\t\x12\x0e\n\x06\x66older\x18\x04 \x01(\t\x12\x0e\n\x06prompt\x18\x05 \x01(\t\x12\x10\n\x08parallel\x18\x06 \x01(\x05\x12%\n\x04vars\x18\x07 \x01(\x0b\x32\x17.google.protobuf.Struct\x12\x0f\n\x07timeout\x18\x08 \x01(\x05\x12\x0c\n\x04name\x18\t \x01(\t\"A\n\rScriptRequest\x12\x0e\n\x06param1\x18\x01 \x01(\t\x12\x0e\n\x06param2\x18\x02 \x01(\t\x12\x10\n\x08parallel\x18\x03 \x01(\x05\"3\n\rExportRequest\x12\x11\n\tfile_path\x18\x01 \x01(\t\x12\x0f\n\x07\x66olders\x18\x02 \x03(\t\"\x1c\n\x0bListRequest\x12\r\n\x05items\x18\x01 \x03(\t\"\xa6\x02\n\nAskRequest\x12\x12\n\ninput_text\x18\x01 \x01(\t\x12\x0e\n\x06\x64ryrun\x18\x02 \x01(\x08\x12,\n\x0c\x63hat_history\x18\x03 \x01(\x0b\x32\x16.google.protobuf.Value\x12\x12\n\nsession_id\x18\x04 \x01(\t\x12\r\n\x05\x64\x65\x62ug\x18\x05 \x01(\x08\x12\x16\n\x0e\x65ngineer_model\x18\x06 \x01(\t\x12\x18\n\x10\x65ngineer_api_key\x18\x07 \x01(\t\x12\x17\n\x0f\x61rchitect_model\x18\x08 \x01(\t\x12\x19\n\x11\x61rchitect_api_key\x18\t \x01(\t\x12\r\n\x05trust\x18\n \x01(\x08\x12\x1b\n\x13\x63onfirmation_answer\x18\x0b \x01(\t\x12\x11\n\tinterrupt\x18\x0c \x01(\x08\"\xc8\x01\n\nAIResponse\x12\x12\n\ntext_chunk\x18\x01 \x01(\t\x12\x10\n\x08is_final\x18\x02 \x01(\x08\x12,\n\x0b\x66ull_result\x18\x03 \x01(\x0b\x32\x17.google.protobuf.Struct\x12\x15\n\rstatus_update\x18\x04 \x01(\t\x12\x15\n\rdebug_message\x18\x05 \x01(\t\x12\x1d\n\x15requires_confirmation\x18\x06 \x01(\x08\x12\x19\n\x11important_message\x18\x07 \x01(\t\"\x1d\n\x0c\x42oolResponse\x12\r\n\x05value\x18\x01 \x01(\x08\"C\n\x0fProviderRequest\x12\x10\n\x08provider\x18\x01 \x01(\t\x12\r\n\x05model\x18\x02 \x01(\t\x12\x0f\n\x07\x61pi_key\x18\x03 \x01(\t\"\x1b\n\nIntRequest\x12\r\n\x05value\x18\x01 \x01(\x05\"p\n\rNodeRunResult\x12\x11\n\tunique_id\x18\x01 \x01(\t\x12\x0e\n\x06output\x18\x02 \x01(\t\x12\x0e\n\x06status\x18\x03 \x01(\x05\x12,\n\x0btest_result\x18\x04 \x01(\x0b\x32\x17.google.protobuf.Struct\"m\n\x12\x46ullReplaceRequest\x12,\n\x0b\x63onnections\x18\x01 \x01(\x0b\x32\x17.google.protobuf.Struct\x12)\n\x08profiles\x18\x02 \x01(\x0b\x32\x17.google.protobuf.Struct\"X\n\x0e\x43opilotRequest\x12\x17\n\x0fterminal_buffer\x18\x01 \x01(\t\x12\x15\n\ruser_question\x18\x02 \x01(\t\x12\x16\n\x0enode_info_json\x18\x03 \x01(\t\"U\n\x0f\x43opilotResponse\x12\x10\n\x08\x63ommands\x18\x01 \x03(\t\x12\r\n\x05guide\x18\x02 \x01(\t\x12\x12\n\nrisk_level\x18\x03 \x01(\t\x12\r\n\x05\x65rror\x18\x04 \x01(\t2\xe1\x07\n\x0bNodeService\x12<\n\nlist_nodes\x12\x15.connpy.FilterRequest\x1a\x15.connpy.ValueResponse\"\x00\x12>\n\x0clist_folders\x12\x15.connpy.FilterRequest\x1a\x15.connpy.ValueResponse\"\x00\x12?\n\x10get_node_details\x12\x11.connpy.IdRequest\x1a\x16.connpy.StructResponse\"\x00\x12<\n\x0e\x65xplode_unique\x12\x11.connpy.IdRequest\x1a\x15.connpy.ValueResponse\"\x00\x12\x42\n\x0egenerate_cache\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\"\x00\x12\x39\n\x08\x61\x64\x64_node\x12\x13.connpy.NodeRequest\x1a\x16.google.protobuf.Empty\"\x00\x12<\n\x0bupdate_node\x12\x13.connpy.NodeRequest\x1a\x16.google.protobuf.Empty\"\x00\x12>\n\x0b\x64\x65lete_node\x12\x15.connpy.DeleteRequest\x1a\x16.google.protobuf.Empty\"\x00\x12:\n\tmove_node\x12\x13.connpy.MoveRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x39\n\x08\x62ulk_add\x12\x13.connpy.BulkRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x45\n\x16validate_parent_folder\x12\x11.connpy.IdRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x43\n\x12set_reserved_names\x12\x13.connpy.ListRequest\x1a\x16.google.protobuf.Empty\"\x00\x12H\n\rinteract_node\x12\x17.connpy.InteractRequest\x1a\x18.connpy.InteractResponse\"\x00(\x01\x30\x01\x12\x44\n\x0c\x66ull_replace\x12\x1a.connpy.FullReplaceRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x45\n\rget_inventory\x12\x16.google.protobuf.Empty\x1a\x1a.connpy.FullReplaceRequest\"\x00\x32\x96\x03\n\x0eProfileService\x12?\n\rlist_profiles\x12\x15.connpy.FilterRequest\x1a\x15.connpy.ValueResponse\"\x00\x12?\n\x0bget_profile\x12\x16.connpy.ProfileRequest\x1a\x16.connpy.StructResponse\"\x00\x12<\n\x0b\x61\x64\x64_profile\x12\x13.connpy.NodeRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x44\n\x11resolve_node_data\x12\x15.connpy.StructRequest\x1a\x16.connpy.StructResponse\"\x00\x12=\n\x0e\x64\x65lete_profile\x12\x11.connpy.IdRequest\x1a\x16.google.protobuf.Empty\"\x00\x12?\n\x0eupdate_profile\x12\x13.connpy.NodeRequest\x1a\x16.google.protobuf.Empty\"\x00\x32\xae\x03\n\rConfigService\x12@\n\x0cget_settings\x12\x16.google.protobuf.Empty\x1a\x16.connpy.StructResponse\"\x00\x12\x43\n\x0fget_default_dir\x12\x16.google.protobuf.Empty\x1a\x16.connpy.StringResponse\"\x00\x12\x44\n\x11set_config_folder\x12\x15.connpy.StringRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x41\n\x0eupdate_setting\x12\x15.connpy.UpdateRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x43\n\x10\x65ncrypt_password\x12\x15.connpy.StringRequest\x1a\x16.connpy.StringResponse\"\x00\x12H\n\x15\x61pply_theme_from_file\x12\x15.connpy.StringRequest\x1a\x16.connpy.StructResponse\"\x00\x32\xca\x02\n\rPluginService\x12?\n\x0clist_plugins\x12\x16.google.protobuf.Empty\x1a\x15.connpy.ValueResponse\"\x00\x12=\n\nadd_plugin\x12\x15.connpy.PluginRequest\x1a\x16.google.protobuf.Empty\"\x00\x12<\n\rdelete_plugin\x12\x11.connpy.IdRequest\x1a\x16.google.protobuf.Empty\"\x00\x12<\n\renable_plugin\x12\x11.connpy.IdRequest\x1a\x16.google.protobuf.Empty\"\x00\x12=\n\x0e\x64isable_plugin\x12\x11.connpy.IdRequest\x1a\x16.google.protobuf.Empty\"\x00\x32\x9b\x02\n\x10\x45xecutionService\x12=\n\x0crun_commands\x12\x12.connpy.RunRequest\x1a\x15.connpy.NodeRunResult\"\x00\x30\x01\x12?\n\rtest_commands\x12\x13.connpy.TestRequest\x1a\x15.connpy.NodeRunResult\"\x00\x30\x01\x12\x41\n\x0erun_cli_script\x12\x15.connpy.ScriptRequest\x1a\x16.connpy.StructResponse\"\x00\x12\x44\n\x11run_yaml_playbook\x12\x15.connpy.ScriptRequest\x1a\x16.connpy.StructResponse\"\x00\x32\xe2\x01\n\x13ImportExportService\x12\x41\n\x0e\x65xport_to_file\x12\x15.connpy.ExportRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x43\n\x10import_from_file\x12\x15.connpy.StringRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x43\n\x12set_reserved_names\x12\x13.connpy.ListRequest\x1a\x16.google.protobuf.Empty\"\x00\x32\xd0\x03\n\tAIService\x12\x33\n\x03\x61sk\x12\x12.connpy.AskRequest\x1a\x12.connpy.AIResponse\"\x00(\x01\x30\x01\x12\x38\n\x07\x63onfirm\x12\x15.connpy.StringRequest\x1a\x14.connpy.BoolResponse\"\x00\x12@\n\x0b\x61sk_copilot\x12\x16.connpy.CopilotRequest\x1a\x17.connpy.CopilotResponse\"\x00\x12@\n\rlist_sessions\x12\x16.google.protobuf.Empty\x1a\x15.connpy.ValueResponse\"\x00\x12\x41\n\x0e\x64\x65lete_session\x12\x15.connpy.StringRequest\x1a\x16.google.protobuf.Empty\"\x00\x12G\n\x12\x63onfigure_provider\x12\x17.connpy.ProviderRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x44\n\x11load_session_data\x12\x15.connpy.StringRequest\x1a\x16.connpy.StructResponse\"\x00\x32\xc2\x02\n\rSystemService\x12\x39\n\tstart_api\x12\x12.connpy.IntRequest\x1a\x16.google.protobuf.Empty\"\x00\x12\x39\n\tdebug_api\x12\x12.connpy.IntRequest\x1a\x16.google.protobuf.Empty\"\x00\x12<\n\x08stop_api\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\"\x00\x12;\n\x0brestart_api\x12\x12.connpy.IntRequest\x1a\x16.google.protobuf.Empty\"\x00\x12@\n\x0eget_api_status\x12\x16.google.protobuf.Empty\x1a\x14.connpy.BoolResponse\"\x00\x62\x06proto3')
_globals = globals()
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
@@ -91,20 +91,24 @@ if not _descriptor._USE_C_DESCRIPTORS:
_globals['_NODERUNRESULT']._serialized_end=2366
_globals['_FULLREPLACEREQUEST']._serialized_start=2368
_globals['_FULLREPLACEREQUEST']._serialized_end=2477
- _globals['_NODESERVICE']._serialized_start=2480
- _globals['_NODESERVICE']._serialized_end=3473
- _globals['_PROFILESERVICE']._serialized_start=3476
- _globals['_PROFILESERVICE']._serialized_end=3882
- _globals['_CONFIGSERVICE']._serialized_start=3885
- _globals['_CONFIGSERVICE']._serialized_end=4315
- _globals['_PLUGINSERVICE']._serialized_start=4318
- _globals['_PLUGINSERVICE']._serialized_end=4648
- _globals['_EXECUTIONSERVICE']._serialized_start=4651
- _globals['_EXECUTIONSERVICE']._serialized_end=4934
- _globals['_IMPORTEXPORTSERVICE']._serialized_start=4937
- _globals['_IMPORTEXPORTSERVICE']._serialized_end=5163
- _globals['_AISERVICE']._serialized_start=5166
- _globals['_AISERVICE']._serialized_end=5564
- _globals['_SYSTEMSERVICE']._serialized_start=5567
- _globals['_SYSTEMSERVICE']._serialized_end=5889
+ _globals['_COPILOTREQUEST']._serialized_start=2479
+ _globals['_COPILOTREQUEST']._serialized_end=2567
+ _globals['_COPILOTRESPONSE']._serialized_start=2569
+ _globals['_COPILOTRESPONSE']._serialized_end=2654
+ _globals['_NODESERVICE']._serialized_start=2657
+ _globals['_NODESERVICE']._serialized_end=3650
+ _globals['_PROFILESERVICE']._serialized_start=3653
+ _globals['_PROFILESERVICE']._serialized_end=4059
+ _globals['_CONFIGSERVICE']._serialized_start=4062
+ _globals['_CONFIGSERVICE']._serialized_end=4492
+ _globals['_PLUGINSERVICE']._serialized_start=4495
+ _globals['_PLUGINSERVICE']._serialized_end=4825
+ _globals['_EXECUTIONSERVICE']._serialized_start=4828
+ _globals['_EXECUTIONSERVICE']._serialized_end=5111
+ _globals['_IMPORTEXPORTSERVICE']._serialized_start=5114
+ _globals['_IMPORTEXPORTSERVICE']._serialized_end=5340
+ _globals['_AISERVICE']._serialized_start=5343
+ _globals['_AISERVICE']._serialized_end=5807
+ _globals['_SYSTEMSERVICE']._serialized_start=5810
+ _globals['_SYSTEMSERVICE']._serialized_end=6132
# @@protoc_insertion_point(module_scope)
diff --git a/connpy/grpc_layer/connpy_pb2_grpc.py b/connpy/grpc_layer/connpy_pb2_grpc.py
index 6975cf7..dd9b5e7 100644
--- a/connpy/grpc_layer/connpy_pb2_grpc.py
+++ b/connpy/grpc_layer/connpy_pb2_grpc.py
@@ -2,7 +2,8 @@
"""Client and server classes corresponding to protobuf-defined services."""
import grpc
import warnings
-from . import connpy_pb2 as connpy__pb2
+
+import connpy_pb2 as connpy__pb2
from google.protobuf import empty_pb2 as google_dot_protobuf_dot_empty__pb2
GRPC_GENERATED_VERSION = '1.80.0'
@@ -1895,6 +1896,11 @@ class AIServiceStub(object):
request_serializer=connpy__pb2.StringRequest.SerializeToString,
response_deserializer=connpy__pb2.BoolResponse.FromString,
_registered_method=True)
+ self.ask_copilot = channel.unary_unary(
+ '/connpy.AIService/ask_copilot',
+ request_serializer=connpy__pb2.CopilotRequest.SerializeToString,
+ response_deserializer=connpy__pb2.CopilotResponse.FromString,
+ _registered_method=True)
self.list_sessions = channel.unary_unary(
'/connpy.AIService/list_sessions',
request_serializer=google_dot_protobuf_dot_empty__pb2.Empty.SerializeToString,
@@ -1932,6 +1938,12 @@ class AIServiceServicer(object):
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')
+ def ask_copilot(self, request, context):
+ """Missing associated documentation comment in .proto file."""
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
def list_sessions(self, request, context):
"""Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
@@ -1969,6 +1981,11 @@ def add_AIServiceServicer_to_server(servicer, server):
request_deserializer=connpy__pb2.StringRequest.FromString,
response_serializer=connpy__pb2.BoolResponse.SerializeToString,
),
+ 'ask_copilot': grpc.unary_unary_rpc_method_handler(
+ servicer.ask_copilot,
+ request_deserializer=connpy__pb2.CopilotRequest.FromString,
+ response_serializer=connpy__pb2.CopilotResponse.SerializeToString,
+ ),
'list_sessions': grpc.unary_unary_rpc_method_handler(
servicer.list_sessions,
request_deserializer=google_dot_protobuf_dot_empty__pb2.Empty.FromString,
@@ -2054,6 +2071,33 @@ class AIService(object):
metadata,
_registered_method=True)
+ @staticmethod
+ def ask_copilot(request,
+ target,
+ options=(),
+ channel_credentials=None,
+ call_credentials=None,
+ insecure=False,
+ compression=None,
+ wait_for_ready=None,
+ timeout=None,
+ metadata=None):
+ return grpc.experimental.unary_unary(
+ request,
+ target,
+ '/connpy.AIService/ask_copilot',
+ connpy__pb2.CopilotRequest.SerializeToString,
+ connpy__pb2.CopilotResponse.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
+
@staticmethod
def list_sessions(request,
target,
diff --git a/connpy/grpc_layer/server.py b/connpy/grpc_layer/server.py
index 3a75ff9..e4f56a0 100644
--- a/connpy/grpc_layer/server.py
+++ b/connpy/grpc_layer/server.py
@@ -748,6 +748,22 @@ class AIServicer(connpy_pb2_grpc.AIServiceServicer):
res = self.service.confirm(request.value)
return connpy_pb2.BoolResponse(value=res)
+ @handle_errors
+ def ask_copilot(self, request, context):
+ import json
+ node_info = json.loads(request.node_info_json) if request.node_info_json else None
+ result = self.service.ask_copilot(
+ request.terminal_buffer,
+ request.user_question,
+ node_info
+ )
+ return connpy_pb2.CopilotResponse(
+ commands=result.get("commands", []),
+ guide=result.get("guide", ""),
+ risk_level=result.get("risk_level", "low"),
+ error=result.get("error") or ""
+ )
+
@handle_errors
def list_sessions(self, request, context):
return connpy_pb2.ValueResponse(data=to_value(self.service.list_sessions()))
diff --git a/connpy/proto/connpy.proto b/connpy/proto/connpy.proto
index bf8546a..5d30f37 100644
--- a/connpy/proto/connpy.proto
+++ b/connpy/proto/connpy.proto
@@ -65,6 +65,7 @@ service ImportExportService {
service AIService {
rpc ask (stream AskRequest) returns (stream AIResponse) {}
rpc confirm (StringRequest) returns (BoolResponse) {}
+ rpc ask_copilot (CopilotRequest) returns (CopilotResponse) {}
rpc list_sessions (google.protobuf.Empty) returns (ValueResponse) {}
rpc delete_session (StringRequest) returns (google.protobuf.Empty) {}
rpc configure_provider (ProviderRequest) returns (google.protobuf.Empty) {}
@@ -257,3 +258,16 @@ message FullReplaceRequest {
google.protobuf.Struct connections = 1;
google.protobuf.Struct profiles = 2;
}
+
+message CopilotRequest {
+ string terminal_buffer = 1;
+ string user_question = 2;
+ string node_info_json = 3;
+}
+
+message CopilotResponse {
+ repeated string commands = 1;
+ string guide = 2;
+ string risk_level = 3;
+ string error = 4;
+}
diff --git a/connpy/services/ai_service.py b/connpy/services/ai_service.py
index 8f511f2..62e39e6 100644
--- a/connpy/services/ai_service.py
+++ b/connpy/services/ai_service.py
@@ -17,6 +17,12 @@ class AIService(BaseService):
agent = ai(self.config, console=console)
return agent.confirm(input_text)
+ def ask_copilot(self, terminal_buffer, user_question, node_info=None):
+ """Ask the AI copilot for terminal assistance."""
+ from connpy.ai import ai
+ agent = ai(self.config)
+ return agent.ask_copilot(terminal_buffer, user_question, node_info)
+
def list_sessions(self):
"""Return a list of all saved AI sessions."""