update pdoc
This commit is contained in:
@@ -90,9 +90,10 @@ el.replaceWith(d);
|
|||||||
|
|
||||||
async def run_session(self,
|
async def run_session(self,
|
||||||
raw_bytes: bytes,
|
raw_bytes: bytes,
|
||||||
cmd_byte_positions: List[tuple],
|
|
||||||
node_info: dict,
|
node_info: dict,
|
||||||
on_ai_call: Callable):
|
on_ai_call: Callable,
|
||||||
|
cmd_byte_positions: List[tuple] = None,
|
||||||
|
blocks: List[tuple] = None):
|
||||||
"""
|
"""
|
||||||
Runs the interactive Copilot session.
|
Runs the interactive Copilot session.
|
||||||
on_ai_call: async function(active_buffer, question) -> result_dict
|
on_ai_call: async function(active_buffer, question) -> result_dict
|
||||||
@@ -102,9 +103,11 @@ el.replaceWith(d);
|
|||||||
try:
|
try:
|
||||||
# Prepare UI state
|
# Prepare UI state
|
||||||
buffer = log_cleaner(raw_bytes.decode(errors='replace'))
|
buffer = log_cleaner(raw_bytes.decode(errors='replace'))
|
||||||
blocks = self.ai_service.build_context_blocks(raw_bytes, cmd_byte_positions, node_info)
|
|
||||||
last_line = buffer.split('\n')[-1].strip() if buffer.strip() else "(prompt)"
|
# Use pre-calculated blocks if provided (remote mode), otherwise calculate locally (local mode)
|
||||||
blocks.append((len(raw_bytes), last_line[:80]))
|
if blocks is None:
|
||||||
|
last_line = buffer.split('\n')[-1].strip() if buffer.strip() else "(prompt)"
|
||||||
|
blocks = self.ai_service.build_context_blocks(raw_bytes, cmd_byte_positions, node_info, last_line=last_line)
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
'context_cmd': 1,
|
'context_cmd': 1,
|
||||||
@@ -121,7 +124,7 @@ el.replaceWith(d);
|
|||||||
self.console.print("") # Salto de línea real
|
self.console.print("") # Salto de línea real
|
||||||
self.console.print(Rule(title="[bold cyan] AI TERMINAL COPILOT [/bold cyan]", style="cyan"))
|
self.console.print(Rule(title="[bold cyan] AI TERMINAL COPILOT [/bold cyan]", style="cyan"))
|
||||||
self.console.print(Panel(
|
self.console.print(Panel(
|
||||||
"[dim]Type your question. Enter to send, Escape/Ctrl+C to cancel.\n"
|
"[dim]Type your question. Enter to send, Escape/Ctrl+C to cancel. Type / for commands.\n"
|
||||||
"Tab to change context mode. Ctrl+\u2191/\u2193 to adjust context. \u2191\u2193 for question history.[/dim]",
|
"Tab to change context mode. Ctrl+\u2191/\u2193 to adjust context. \u2191\u2193 for question history.[/dim]",
|
||||||
border_style="cyan"
|
border_style="cyan"
|
||||||
))
|
))
|
||||||
@@ -161,9 +164,8 @@ el.replaceWith(d);
|
|||||||
if state['context_mode'] == self.mode_lines:
|
if state['context_mode'] == self.mode_lines:
|
||||||
return '\n'.join(buffer.split('\n')[-state['context_lines']:])
|
return '\n'.join(buffer.split('\n')[-state['context_lines']:])
|
||||||
idx = max(0, state['total_cmds'] - state['context_cmd'])
|
idx = max(0, state['total_cmds'] - state['context_cmd'])
|
||||||
start, preview = blocks[idx]
|
start, end, preview = blocks[idx]
|
||||||
if state['context_mode'] == self.mode_single and idx + 1 < state['total_cmds']:
|
if state['context_mode'] == self.mode_single:
|
||||||
end = blocks[idx + 1][0]
|
|
||||||
active_raw = raw_bytes[start:end]
|
active_raw = raw_bytes[start:end]
|
||||||
else:
|
else:
|
||||||
active_raw = raw_bytes[start:]
|
active_raw = raw_bytes[start:]
|
||||||
@@ -205,7 +207,40 @@ el.replaceWith(d);
|
|||||||
base_str = f'\u25b6 Ctrl+\u2191/\u2193 adjusts by 50 lines [Tab: {m_label}]'
|
base_str = f'\u25b6 Ctrl+\u2191/\u2193 adjusts by 50 lines [Tab: {m_label}]'
|
||||||
else:
|
else:
|
||||||
idx = max(0, state['total_cmds'] - state['context_cmd'])
|
idx = max(0, state['total_cmds'] - state['context_cmd'])
|
||||||
desc = blocks[idx][1]
|
import re
|
||||||
|
|
||||||
|
def clean_preview(text):
|
||||||
|
# Limpia saltos de línea y el prompt inicial (todo hasta #, > o $) para que quede solo el comando
|
||||||
|
original = text.strip().replace('\r', '').replace('\n', ' ')
|
||||||
|
cleaned = re.sub(r'^.*?[#>\$]\s*', '', original)
|
||||||
|
# Si limpiar el prompt nos deja con un string vacío (ej: era solo "iol#"), devolvemos el original
|
||||||
|
return cleaned if cleaned else original
|
||||||
|
|
||||||
|
if state['context_mode'] == self.mode_range:
|
||||||
|
range_blocks = blocks[idx:]
|
||||||
|
# Si hay más de un bloque, el último es siempre el prompt vacío/actual. Lo omitimos visualmente.
|
||||||
|
if len(range_blocks) > 1:
|
||||||
|
range_blocks = range_blocks[:-1]
|
||||||
|
|
||||||
|
# Limpiar y truncar comandos muy largos para que no rompan la UI
|
||||||
|
previews = []
|
||||||
|
for b in range_blocks:
|
||||||
|
p = clean_preview(b[2])
|
||||||
|
if p:
|
||||||
|
# Truncar comandos individuales largos
|
||||||
|
if len(p) > 25: p = p[:22] + "..."
|
||||||
|
previews.append(p)
|
||||||
|
|
||||||
|
if not previews:
|
||||||
|
desc = clean_preview(blocks[idx][2])
|
||||||
|
elif len(previews) <= 3:
|
||||||
|
desc = " + ".join(previews)
|
||||||
|
else:
|
||||||
|
desc = f"{previews[0]} + {previews[1]} + {previews[2]} ... (+{len(previews)-3})"
|
||||||
|
else:
|
||||||
|
# Modo SINGLE original
|
||||||
|
desc = clean_preview(blocks[idx][2])
|
||||||
|
|
||||||
base_str = f'\u25b6 {desc} [Tab: {m_label}]'
|
base_str = f'\u25b6 {desc} [Tab: {m_label}]'
|
||||||
|
|
||||||
# Wrap base_str in a style to maintain consistency and avoid glitches
|
# Wrap base_str in a style to maintain consistency and avoid glitches
|
||||||
@@ -332,39 +367,67 @@ el.replaceWith(d);
|
|||||||
# Use persona from overrides (one-shot) or from session state
|
# Use persona from overrides (one-shot) or from session state
|
||||||
active_persona = merged_node_info.get('persona', self.session_state.get('persona', 'engineer'))
|
active_persona = merged_node_info.get('persona', self.session_state.get('persona', 'engineer'))
|
||||||
persona_color = self._get_theme_color(active_persona, fallback="cyan")
|
persona_color = self._get_theme_color(active_persona, fallback="cyan")
|
||||||
|
persona_title = "Network Architect" if active_persona == "architect" else "Network Engineer"
|
||||||
|
|
||||||
active_buffer = get_active_buffer()
|
active_buffer = get_active_buffer()
|
||||||
live_text = "Thinking..."
|
live_text = ""
|
||||||
panel = Panel(live_text, title=f"[bold {persona_color}]Copilot Guide[/bold {persona_color}]", border_style=persona_color)
|
first_chunk = True
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from rich.rule import Rule
|
||||||
|
from rich.status import Status
|
||||||
|
from connpy.printer import IncrementalMarkdownParser
|
||||||
|
|
||||||
|
md_parser = IncrementalMarkdownParser(console=self.console)
|
||||||
|
|
||||||
|
status_spinner = Status(
|
||||||
|
f"[bold {persona_color}]{persona_title}:[/bold {persona_color}] [dim]Thinking...[/dim]",
|
||||||
|
console=self.console,
|
||||||
|
spinner="dots"
|
||||||
|
)
|
||||||
|
status_spinner.start()
|
||||||
|
|
||||||
def on_chunk(text):
|
def on_chunk(text):
|
||||||
nonlocal live_text
|
nonlocal live_text, first_chunk
|
||||||
if live_text == "Thinking...": live_text = ""
|
if first_chunk:
|
||||||
|
status_spinner.stop()
|
||||||
|
# Print header rule before first chunk arrives
|
||||||
|
self.console.print(Rule(
|
||||||
|
f"[bold {persona_color}]{persona_title}[/bold {persona_color}]",
|
||||||
|
style=persona_color
|
||||||
|
))
|
||||||
|
first_chunk = False
|
||||||
live_text += text
|
live_text += text
|
||||||
|
md_parser.feed(text)
|
||||||
|
|
||||||
with Live(panel, console=self.console, refresh_per_second=10) as live:
|
# Check for interruption during AI call
|
||||||
def update_live(t):
|
ai_task = asyncio.create_task(on_ai_call(active_buffer, clean_question, on_chunk, merged_node_info))
|
||||||
live.update(Panel(Markdown(t), title=f"[bold {persona_color}]Copilot Guide[/bold {persona_color}]", border_style=persona_color))
|
|
||||||
|
try:
|
||||||
wrapped_chunk = lambda t: (on_chunk(t), update_live(live_text))
|
while not ai_task.done():
|
||||||
|
await asyncio.sleep(0.05)
|
||||||
# Check for interruption during AI call
|
result = await ai_task
|
||||||
ai_task = asyncio.create_task(on_ai_call(active_buffer, clean_question, wrapped_chunk, merged_node_info))
|
except asyncio.CancelledError:
|
||||||
|
status_spinner.stop()
|
||||||
try:
|
return "cancel", None, None
|
||||||
while not ai_task.done():
|
|
||||||
await asyncio.sleep(0.05)
|
# Ensure spinner is stopped if no chunks arrived
|
||||||
result = await ai_task
|
if first_chunk:
|
||||||
except asyncio.CancelledError:
|
status_spinner.stop()
|
||||||
return "cancel", None, None
|
|
||||||
|
|
||||||
|
# Close the streamed output with a Rule
|
||||||
|
if not first_chunk:
|
||||||
|
md_parser.flush()
|
||||||
|
self.console.print(Rule(style=persona_color))
|
||||||
|
|
||||||
if not result or result.get("error"):
|
if not result or result.get("error"):
|
||||||
if result and result.get("error"): self.console.print(f"[red]Error: {result['error']}[/red]")
|
if first_chunk and result and result.get("error"):
|
||||||
|
self.console.print(f"[red]Error: {result['error']}[/red]")
|
||||||
return "cancel", None, None
|
return "cancel", None, None
|
||||||
|
|
||||||
# 4. Handle result
|
# If no chunks were streamed but we have a guide, print it as a panel
|
||||||
if live_text == "Thinking..." and result.get("guide"):
|
if first_chunk and result and result.get("guide"):
|
||||||
self.console.print(Panel(Markdown(result["guide"]), title=f"[bold {persona_color}]Copilot Guide[/bold {persona_color}]", border_style=persona_color))
|
self.console.print(Panel(Markdown(result["guide"]), title=f"[bold {persona_color}]{persona_title}[/bold {persona_color}]", border_style=persona_color))
|
||||||
|
|
||||||
commands = result.get("commands", [])
|
commands = result.get("commands", [])
|
||||||
if not commands:
|
if not commands:
|
||||||
@@ -466,14 +529,13 @@ el.replaceWith(d);
|
|||||||
return "cancel", None, None
|
return "cancel", None, None
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
state['cancelled'] = True
|
state['cancelled'] = True</code></pre>
|
||||||
self.console.print("[dim]Returning to session...[/dim]")</code></pre>
|
|
||||||
</details>
|
</details>
|
||||||
<div class="desc"></div>
|
<div class="desc"></div>
|
||||||
<h3>Methods</h3>
|
<h3>Methods</h3>
|
||||||
<dl>
|
<dl>
|
||||||
<dt id="connpy.cli.terminal_ui.CopilotInterface.run_session"><code class="name flex">
|
<dt id="connpy.cli.terminal_ui.CopilotInterface.run_session"><code class="name flex">
|
||||||
<span>async def <span class="ident">run_session</span></span>(<span>self,<br>raw_bytes: bytes,<br>cmd_byte_positions: List[tuple],<br>node_info: dict,<br>on_ai_call: Callable)</span>
|
<span>async def <span class="ident">run_session</span></span>(<span>self,<br>raw_bytes: bytes,<br>node_info: dict,<br>on_ai_call: Callable,<br>cmd_byte_positions: List[tuple] = None,<br>blocks: List[tuple] = None)</span>
|
||||||
</code></dt>
|
</code></dt>
|
||||||
<dd>
|
<dd>
|
||||||
<details class="source">
|
<details class="source">
|
||||||
@@ -482,9 +544,10 @@ el.replaceWith(d);
|
|||||||
</summary>
|
</summary>
|
||||||
<pre><code class="python">async def run_session(self,
|
<pre><code class="python">async def run_session(self,
|
||||||
raw_bytes: bytes,
|
raw_bytes: bytes,
|
||||||
cmd_byte_positions: List[tuple],
|
|
||||||
node_info: dict,
|
node_info: dict,
|
||||||
on_ai_call: Callable):
|
on_ai_call: Callable,
|
||||||
|
cmd_byte_positions: List[tuple] = None,
|
||||||
|
blocks: List[tuple] = None):
|
||||||
"""
|
"""
|
||||||
Runs the interactive Copilot session.
|
Runs the interactive Copilot session.
|
||||||
on_ai_call: async function(active_buffer, question) -> result_dict
|
on_ai_call: async function(active_buffer, question) -> result_dict
|
||||||
@@ -494,9 +557,11 @@ el.replaceWith(d);
|
|||||||
try:
|
try:
|
||||||
# Prepare UI state
|
# Prepare UI state
|
||||||
buffer = log_cleaner(raw_bytes.decode(errors='replace'))
|
buffer = log_cleaner(raw_bytes.decode(errors='replace'))
|
||||||
blocks = self.ai_service.build_context_blocks(raw_bytes, cmd_byte_positions, node_info)
|
|
||||||
last_line = buffer.split('\n')[-1].strip() if buffer.strip() else "(prompt)"
|
# Use pre-calculated blocks if provided (remote mode), otherwise calculate locally (local mode)
|
||||||
blocks.append((len(raw_bytes), last_line[:80]))
|
if blocks is None:
|
||||||
|
last_line = buffer.split('\n')[-1].strip() if buffer.strip() else "(prompt)"
|
||||||
|
blocks = self.ai_service.build_context_blocks(raw_bytes, cmd_byte_positions, node_info, last_line=last_line)
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
'context_cmd': 1,
|
'context_cmd': 1,
|
||||||
@@ -513,7 +578,7 @@ el.replaceWith(d);
|
|||||||
self.console.print("") # Salto de línea real
|
self.console.print("") # Salto de línea real
|
||||||
self.console.print(Rule(title="[bold cyan] AI TERMINAL COPILOT [/bold cyan]", style="cyan"))
|
self.console.print(Rule(title="[bold cyan] AI TERMINAL COPILOT [/bold cyan]", style="cyan"))
|
||||||
self.console.print(Panel(
|
self.console.print(Panel(
|
||||||
"[dim]Type your question. Enter to send, Escape/Ctrl+C to cancel.\n"
|
"[dim]Type your question. Enter to send, Escape/Ctrl+C to cancel. Type / for commands.\n"
|
||||||
"Tab to change context mode. Ctrl+\u2191/\u2193 to adjust context. \u2191\u2193 for question history.[/dim]",
|
"Tab to change context mode. Ctrl+\u2191/\u2193 to adjust context. \u2191\u2193 for question history.[/dim]",
|
||||||
border_style="cyan"
|
border_style="cyan"
|
||||||
))
|
))
|
||||||
@@ -553,9 +618,8 @@ el.replaceWith(d);
|
|||||||
if state['context_mode'] == self.mode_lines:
|
if state['context_mode'] == self.mode_lines:
|
||||||
return '\n'.join(buffer.split('\n')[-state['context_lines']:])
|
return '\n'.join(buffer.split('\n')[-state['context_lines']:])
|
||||||
idx = max(0, state['total_cmds'] - state['context_cmd'])
|
idx = max(0, state['total_cmds'] - state['context_cmd'])
|
||||||
start, preview = blocks[idx]
|
start, end, preview = blocks[idx]
|
||||||
if state['context_mode'] == self.mode_single and idx + 1 < state['total_cmds']:
|
if state['context_mode'] == self.mode_single:
|
||||||
end = blocks[idx + 1][0]
|
|
||||||
active_raw = raw_bytes[start:end]
|
active_raw = raw_bytes[start:end]
|
||||||
else:
|
else:
|
||||||
active_raw = raw_bytes[start:]
|
active_raw = raw_bytes[start:]
|
||||||
@@ -597,7 +661,40 @@ el.replaceWith(d);
|
|||||||
base_str = f'\u25b6 Ctrl+\u2191/\u2193 adjusts by 50 lines [Tab: {m_label}]'
|
base_str = f'\u25b6 Ctrl+\u2191/\u2193 adjusts by 50 lines [Tab: {m_label}]'
|
||||||
else:
|
else:
|
||||||
idx = max(0, state['total_cmds'] - state['context_cmd'])
|
idx = max(0, state['total_cmds'] - state['context_cmd'])
|
||||||
desc = blocks[idx][1]
|
import re
|
||||||
|
|
||||||
|
def clean_preview(text):
|
||||||
|
# Limpia saltos de línea y el prompt inicial (todo hasta #, > o $) para que quede solo el comando
|
||||||
|
original = text.strip().replace('\r', '').replace('\n', ' ')
|
||||||
|
cleaned = re.sub(r'^.*?[#>\$]\s*', '', original)
|
||||||
|
# Si limpiar el prompt nos deja con un string vacío (ej: era solo "iol#"), devolvemos el original
|
||||||
|
return cleaned if cleaned else original
|
||||||
|
|
||||||
|
if state['context_mode'] == self.mode_range:
|
||||||
|
range_blocks = blocks[idx:]
|
||||||
|
# Si hay más de un bloque, el último es siempre el prompt vacío/actual. Lo omitimos visualmente.
|
||||||
|
if len(range_blocks) > 1:
|
||||||
|
range_blocks = range_blocks[:-1]
|
||||||
|
|
||||||
|
# Limpiar y truncar comandos muy largos para que no rompan la UI
|
||||||
|
previews = []
|
||||||
|
for b in range_blocks:
|
||||||
|
p = clean_preview(b[2])
|
||||||
|
if p:
|
||||||
|
# Truncar comandos individuales largos
|
||||||
|
if len(p) > 25: p = p[:22] + "..."
|
||||||
|
previews.append(p)
|
||||||
|
|
||||||
|
if not previews:
|
||||||
|
desc = clean_preview(blocks[idx][2])
|
||||||
|
elif len(previews) <= 3:
|
||||||
|
desc = " + ".join(previews)
|
||||||
|
else:
|
||||||
|
desc = f"{previews[0]} + {previews[1]} + {previews[2]} ... (+{len(previews)-3})"
|
||||||
|
else:
|
||||||
|
# Modo SINGLE original
|
||||||
|
desc = clean_preview(blocks[idx][2])
|
||||||
|
|
||||||
base_str = f'\u25b6 {desc} [Tab: {m_label}]'
|
base_str = f'\u25b6 {desc} [Tab: {m_label}]'
|
||||||
|
|
||||||
# Wrap base_str in a style to maintain consistency and avoid glitches
|
# Wrap base_str in a style to maintain consistency and avoid glitches
|
||||||
@@ -724,39 +821,67 @@ el.replaceWith(d);
|
|||||||
# Use persona from overrides (one-shot) or from session state
|
# Use persona from overrides (one-shot) or from session state
|
||||||
active_persona = merged_node_info.get('persona', self.session_state.get('persona', 'engineer'))
|
active_persona = merged_node_info.get('persona', self.session_state.get('persona', 'engineer'))
|
||||||
persona_color = self._get_theme_color(active_persona, fallback="cyan")
|
persona_color = self._get_theme_color(active_persona, fallback="cyan")
|
||||||
|
persona_title = "Network Architect" if active_persona == "architect" else "Network Engineer"
|
||||||
|
|
||||||
active_buffer = get_active_buffer()
|
active_buffer = get_active_buffer()
|
||||||
live_text = "Thinking..."
|
live_text = ""
|
||||||
panel = Panel(live_text, title=f"[bold {persona_color}]Copilot Guide[/bold {persona_color}]", border_style=persona_color)
|
first_chunk = True
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from rich.rule import Rule
|
||||||
|
from rich.status import Status
|
||||||
|
from connpy.printer import IncrementalMarkdownParser
|
||||||
|
|
||||||
|
md_parser = IncrementalMarkdownParser(console=self.console)
|
||||||
|
|
||||||
|
status_spinner = Status(
|
||||||
|
f"[bold {persona_color}]{persona_title}:[/bold {persona_color}] [dim]Thinking...[/dim]",
|
||||||
|
console=self.console,
|
||||||
|
spinner="dots"
|
||||||
|
)
|
||||||
|
status_spinner.start()
|
||||||
|
|
||||||
def on_chunk(text):
|
def on_chunk(text):
|
||||||
nonlocal live_text
|
nonlocal live_text, first_chunk
|
||||||
if live_text == "Thinking...": live_text = ""
|
if first_chunk:
|
||||||
|
status_spinner.stop()
|
||||||
|
# Print header rule before first chunk arrives
|
||||||
|
self.console.print(Rule(
|
||||||
|
f"[bold {persona_color}]{persona_title}[/bold {persona_color}]",
|
||||||
|
style=persona_color
|
||||||
|
))
|
||||||
|
first_chunk = False
|
||||||
live_text += text
|
live_text += text
|
||||||
|
md_parser.feed(text)
|
||||||
|
|
||||||
with Live(panel, console=self.console, refresh_per_second=10) as live:
|
# Check for interruption during AI call
|
||||||
def update_live(t):
|
ai_task = asyncio.create_task(on_ai_call(active_buffer, clean_question, on_chunk, merged_node_info))
|
||||||
live.update(Panel(Markdown(t), title=f"[bold {persona_color}]Copilot Guide[/bold {persona_color}]", border_style=persona_color))
|
|
||||||
|
try:
|
||||||
wrapped_chunk = lambda t: (on_chunk(t), update_live(live_text))
|
while not ai_task.done():
|
||||||
|
await asyncio.sleep(0.05)
|
||||||
# Check for interruption during AI call
|
result = await ai_task
|
||||||
ai_task = asyncio.create_task(on_ai_call(active_buffer, clean_question, wrapped_chunk, merged_node_info))
|
except asyncio.CancelledError:
|
||||||
|
status_spinner.stop()
|
||||||
try:
|
return "cancel", None, None
|
||||||
while not ai_task.done():
|
|
||||||
await asyncio.sleep(0.05)
|
# Ensure spinner is stopped if no chunks arrived
|
||||||
result = await ai_task
|
if first_chunk:
|
||||||
except asyncio.CancelledError:
|
status_spinner.stop()
|
||||||
return "cancel", None, None
|
|
||||||
|
|
||||||
|
# Close the streamed output with a Rule
|
||||||
|
if not first_chunk:
|
||||||
|
md_parser.flush()
|
||||||
|
self.console.print(Rule(style=persona_color))
|
||||||
|
|
||||||
if not result or result.get("error"):
|
if not result or result.get("error"):
|
||||||
if result and result.get("error"): self.console.print(f"[red]Error: {result['error']}[/red]")
|
if first_chunk and result and result.get("error"):
|
||||||
|
self.console.print(f"[red]Error: {result['error']}[/red]")
|
||||||
return "cancel", None, None
|
return "cancel", None, None
|
||||||
|
|
||||||
# 4. Handle result
|
# If no chunks were streamed but we have a guide, print it as a panel
|
||||||
if live_text == "Thinking..." and result.get("guide"):
|
if first_chunk and result and result.get("guide"):
|
||||||
self.console.print(Panel(Markdown(result["guide"]), title=f"[bold {persona_color}]Copilot Guide[/bold {persona_color}]", border_style=persona_color))
|
self.console.print(Panel(Markdown(result["guide"]), title=f"[bold {persona_color}]{persona_title}[/bold {persona_color}]", border_style=persona_color))
|
||||||
|
|
||||||
commands = result.get("commands", [])
|
commands = result.get("commands", [])
|
||||||
if not commands:
|
if not commands:
|
||||||
@@ -858,8 +983,7 @@ el.replaceWith(d);
|
|||||||
return "cancel", None, None
|
return "cancel", None, None
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
state['cancelled'] = True
|
state['cancelled'] = True</code></pre>
|
||||||
self.console.print("[dim]Returning to session...[/dim]")</code></pre>
|
|
||||||
</details>
|
</details>
|
||||||
<div class="desc"><p>Runs the interactive Copilot session.
|
<div class="desc"><p>Runs the interactive Copilot session.
|
||||||
on_ai_call: async function(active_buffer, question) -> result_dict</p></div>
|
on_ai_call: async function(active_buffer, question) -> result_dict</p></div>
|
||||||
|
|||||||
@@ -807,15 +807,34 @@ interceptor chooses to service this RPC, or None otherwise.</p></div>
|
|||||||
import json
|
import json
|
||||||
import asyncio
|
import asyncio
|
||||||
import os
|
import os
|
||||||
|
from ..services.ai_service import AIService
|
||||||
|
|
||||||
|
service = AIService(self.service.config)
|
||||||
|
|
||||||
if node_info is None:
|
if node_info is None:
|
||||||
node_info = {}
|
node_info = {}
|
||||||
|
|
||||||
|
# Calculate real command blocks from history using the central service
|
||||||
|
raw_bytes = n.mylog.getvalue() if hasattr(n, 'mylog') else buffer
|
||||||
|
if not isinstance(raw_bytes, bytes):
|
||||||
|
raw_bytes = str(raw_bytes).encode()
|
||||||
|
|
||||||
|
from connpy.utils import log_cleaner
|
||||||
|
last_line = log_cleaner(raw_bytes.decode(errors='replace')).split('\n')[-1].strip()
|
||||||
|
blocks = service.build_context_blocks(raw_bytes, n.cmd_byte_positions, node_info, last_line=last_line)
|
||||||
|
node_info["context_blocks"] = blocks
|
||||||
|
|
||||||
node_info_json = json.dumps(node_info)
|
node_info_json = json.dumps(node_info)
|
||||||
|
|
||||||
# Convert buffer to string if it's bytes for the preview
|
# Convert buffer to string if it's bytes for the preview
|
||||||
preview_str = buffer[-200:].decode(errors='replace') if isinstance(buffer, bytes) else str(buffer)[-200:]
|
preview_str = buffer[-200:].decode(errors='replace') if isinstance(buffer, bytes) else str(buffer)[-200:]
|
||||||
|
|
||||||
|
# Generate a unique session ID for this copilot interaction to prevent race conditions
|
||||||
|
import uuid
|
||||||
|
copilot_session_id = str(uuid.uuid4())
|
||||||
|
node_info["session_id"] = copilot_session_id
|
||||||
|
node_info_json = json.dumps(node_info)
|
||||||
|
|
||||||
# 1. Send prompt to client
|
# 1. Send prompt to client
|
||||||
response_queue.put(connpy_pb2.InteractResponse(
|
response_queue.put(connpy_pb2.InteractResponse(
|
||||||
copilot_prompt=True,
|
copilot_prompt=True,
|
||||||
@@ -824,6 +843,13 @@ interceptor chooses to service this RPC, or None otherwise.</p></div>
|
|||||||
))
|
))
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
|
# 0. Drain the queue of any stale messages before starting a new interaction
|
||||||
|
while not remote_stream.copilot_queue.empty():
|
||||||
|
try:
|
||||||
|
remote_stream.copilot_queue.get_nowait()
|
||||||
|
except:
|
||||||
|
break
|
||||||
|
|
||||||
# 2. Await the question from client via the copilot_queue
|
# 2. Await the question from client via the copilot_queue
|
||||||
import threading
|
import threading
|
||||||
def preload_ai_deps():
|
def preload_ai_deps():
|
||||||
@@ -836,8 +862,17 @@ interceptor chooses to service this RPC, or None otherwise.</p></div>
|
|||||||
try:
|
try:
|
||||||
req_data = await asyncio.wait_for(remote_stream.copilot_queue.get(), timeout=120)
|
req_data = await asyncio.wait_for(remote_stream.copilot_queue.get(), timeout=120)
|
||||||
if not req_data: return
|
if not req_data: return
|
||||||
if "question" not in req_data or not req_data["question"] or req_data["question"] == "CANCEL" or req_data.get("action") == "cancel":
|
|
||||||
os.write(child_fd, b'\x15\r')
|
# Validate session ID if provided by client (skip validation if not provided for CLI compatibility)
|
||||||
|
req_session_id = req_data.get("session_id")
|
||||||
|
if req_session_id and req_session_id != copilot_session_id:
|
||||||
|
continue # Ignore stale request from a previous session
|
||||||
|
|
||||||
|
if "question" not in req_data or not req_data["question"] or req_data["question"] == "CANCEL" or req_data.get("action") in ("cancel", "web_cancel"):
|
||||||
|
if req_data.get("action") == "web_cancel":
|
||||||
|
os.write(child_fd, b'\x05')
|
||||||
|
else:
|
||||||
|
os.write(child_fd, b'\x15\r')
|
||||||
return
|
return
|
||||||
question = req_data["question"]
|
question = req_data["question"]
|
||||||
|
|
||||||
@@ -864,9 +899,6 @@ interceptor chooses to service this RPC, or None otherwise.</p></div>
|
|||||||
return
|
return
|
||||||
|
|
||||||
# 3. Call AI Service with streaming
|
# 3. Call AI Service with streaming
|
||||||
from ..services.ai_service import AIService
|
|
||||||
service = AIService(self.service.config)
|
|
||||||
|
|
||||||
def chunk_callback(chunk_text):
|
def chunk_callback(chunk_text):
|
||||||
if chunk_text:
|
if chunk_text:
|
||||||
response_queue.put(connpy_pb2.InteractResponse(
|
response_queue.put(connpy_pb2.InteractResponse(
|
||||||
@@ -887,8 +919,11 @@ interceptor chooses to service this RPC, or None otherwise.</p></div>
|
|||||||
if wait_action_task in done:
|
if wait_action_task in done:
|
||||||
req_data = wait_action_task.result()
|
req_data = wait_action_task.result()
|
||||||
ai_task.cancel()
|
ai_task.cancel()
|
||||||
if req_data.get("action") == "cancel" or req_data.get("question") == "CANCEL":
|
if req_data.get("action") in ("cancel", "web_cancel") or req_data.get("question") == "CANCEL":
|
||||||
os.write(child_fd, b'\x15\r')
|
if req_data.get("action") == "web_cancel":
|
||||||
|
os.write(child_fd, b'\x05')
|
||||||
|
else:
|
||||||
|
os.write(child_fd, b'\x15\r')
|
||||||
return
|
return
|
||||||
continue # Loop back instead of returning to keep session alive
|
continue # Loop back instead of returning to keep session alive
|
||||||
else:
|
else:
|
||||||
@@ -912,45 +947,27 @@ interceptor chooses to service this RPC, or None otherwise.</p></div>
|
|||||||
if action == "continue":
|
if action == "continue":
|
||||||
continue # Loop back for next question
|
continue # Loop back for next question
|
||||||
|
|
||||||
if action == "cancel":
|
if action in ("cancel", "web_cancel"):
|
||||||
os.write(child_fd, b'\x15\r')
|
if action == "web_cancel":
|
||||||
|
os.write(child_fd, b'\x05')
|
||||||
|
else:
|
||||||
|
os.write(child_fd, b'\x15\r')
|
||||||
return
|
return
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
os.write(child_fd, b'\x15\r')
|
os.write(child_fd, b'\x15\r')
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def on_inject(cmd):
|
||||||
|
response_queue.put(connpy_pb2.InteractResponse(copilot_injected_command=cmd))
|
||||||
|
|
||||||
if action == "send_all":
|
if action == "send_all":
|
||||||
commands = result.get("commands", [])
|
commands = result.get("commands", [])
|
||||||
os.write(child_fd, b'\x15') # Ctrl+U to clear line
|
await n.inject_commands(commands, child_fd, on_inject=on_inject)
|
||||||
await asyncio.sleep(0.1)
|
|
||||||
|
|
||||||
# Prepend screen length command to avoid pagination
|
|
||||||
if "screen_length_command" in n.tags:
|
|
||||||
os.write(child_fd, (n.tags["screen_length_command"] + "\n").encode())
|
|
||||||
response_queue.put(connpy_pb2.InteractResponse(copilot_injected_command=n.tags["screen_length_command"]))
|
|
||||||
await asyncio.sleep(0.8)
|
|
||||||
|
|
||||||
for cmd in commands:
|
|
||||||
os.write(child_fd, (cmd + "\n").encode())
|
|
||||||
response_queue.put(connpy_pb2.InteractResponse(copilot_injected_command=cmd))
|
|
||||||
await asyncio.sleep(0.8)
|
|
||||||
return
|
return
|
||||||
elif action.startswith("custom:"):
|
elif action.startswith("custom:"):
|
||||||
custom_cmds = action[7:]
|
custom_cmds_raw = action[7:]
|
||||||
os.write(child_fd, b'\x15')
|
custom_cmds = [cmd.strip() for cmd in custom_cmds_raw.split('\n') if cmd.strip()]
|
||||||
await asyncio.sleep(0.1)
|
await n.inject_commands(custom_cmds, child_fd, on_inject=on_inject)
|
||||||
|
|
||||||
# Prepend screen length command to avoid pagination
|
|
||||||
if "screen_length_command" in n.tags:
|
|
||||||
os.write(child_fd, (n.tags["screen_length_command"] + "\n").encode())
|
|
||||||
response_queue.put(connpy_pb2.InteractResponse(copilot_injected_command=n.tags["screen_length_command"]))
|
|
||||||
await asyncio.sleep(0.8)
|
|
||||||
|
|
||||||
for cmd in custom_cmds.split('\n'):
|
|
||||||
if cmd.strip():
|
|
||||||
os.write(child_fd, (cmd.strip() + "\n").encode())
|
|
||||||
response_queue.put(connpy_pb2.InteractResponse(copilot_injected_command=cmd.strip()))
|
|
||||||
await asyncio.sleep(0.8)
|
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
os.write(child_fd, b'\x15\r')
|
os.write(child_fd, b'\x15\r')
|
||||||
|
|||||||
@@ -104,7 +104,6 @@ el.replaceWith(d);
|
|||||||
import queue
|
import queue
|
||||||
from rich.prompt import Prompt
|
from rich.prompt import Prompt
|
||||||
from rich.text import Text
|
from rich.text import Text
|
||||||
from rich.live import Live
|
|
||||||
from rich.panel import Panel
|
from rich.panel import Panel
|
||||||
from rich.markdown import Markdown
|
from rich.markdown import Markdown
|
||||||
|
|
||||||
@@ -135,7 +134,7 @@ el.replaceWith(d);
|
|||||||
responses = self.stub.ask(request_generator())
|
responses = self.stub.ask(request_generator())
|
||||||
|
|
||||||
full_content = ""
|
full_content = ""
|
||||||
live_display = None
|
header_printed = False
|
||||||
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
|
||||||
@@ -200,69 +199,53 @@ el.replaceWith(d);
|
|||||||
|
|
||||||
if response.debug_message:
|
if response.debug_message:
|
||||||
if debug:
|
if debug:
|
||||||
if live_display:
|
|
||||||
try: live_display.stop()
|
|
||||||
except: pass
|
|
||||||
if status:
|
if status:
|
||||||
try: status.stop()
|
try: status.stop()
|
||||||
except: pass
|
except: pass
|
||||||
printer.console.print(Text.from_ansi(response.debug_message))
|
printer.console.print(Text.from_ansi(response.debug_message))
|
||||||
if live_display:
|
if status:
|
||||||
try: live_display.start()
|
|
||||||
except: pass
|
|
||||||
elif status:
|
|
||||||
try: status.start()
|
try: status.start()
|
||||||
except: pass
|
except: pass
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if response.important_message:
|
if response.important_message:
|
||||||
if live_display:
|
|
||||||
try: live_display.stop()
|
|
||||||
except: pass
|
|
||||||
if status:
|
if status:
|
||||||
try: status.stop()
|
try: status.stop()
|
||||||
except: pass
|
except: pass
|
||||||
printer.console.print(Text.from_ansi(response.important_message))
|
printer.console.print(Text.from_ansi(response.important_message))
|
||||||
if live_display:
|
if status:
|
||||||
try: live_display.start()
|
|
||||||
except: pass
|
|
||||||
elif status:
|
|
||||||
try: status.start()
|
try: status.start()
|
||||||
except: pass
|
except: pass
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if not response.is_final:
|
if not response.is_final:
|
||||||
if response.text_chunk:
|
if response.text_chunk:
|
||||||
full_content += response.text_chunk
|
if not header_printed:
|
||||||
|
|
||||||
if not live_display:
|
|
||||||
if status:
|
if status:
|
||||||
try: status.stop()
|
try: status.stop()
|
||||||
except: pass
|
except: pass
|
||||||
|
|
||||||
from rich.console import Console as RichConsole
|
from rich.console import Console as RichConsole
|
||||||
from ..printer import connpy_theme, get_original_stdout
|
from rich.rule import Rule
|
||||||
|
from ..printer import connpy_theme, get_original_stdout, IncrementalMarkdownParser
|
||||||
stable_console = RichConsole(theme=connpy_theme, file=get_original_stdout())
|
stable_console = RichConsole(theme=connpy_theme, file=get_original_stdout())
|
||||||
|
|
||||||
# We default to Engineer title during stream, final result will correct it if needed
|
# Print header on first chunk
|
||||||
live_display = Live(
|
stable_console.print(Rule("[bold engineer]Network Engineer[/bold engineer]", style="engineer"))
|
||||||
Panel(Markdown(full_content), title="[bold engineer]Network Engineer[/bold engineer]", border_style="engineer", expand=False),
|
header_printed = True
|
||||||
console=stable_console,
|
|
||||||
refresh_per_second=8,
|
# Initialize parser
|
||||||
transient=False
|
md_parser = IncrementalMarkdownParser(console=stable_console)
|
||||||
)
|
|
||||||
live_display.start()
|
full_content += response.text_chunk
|
||||||
else:
|
md_parser.feed(response.text_chunk)
|
||||||
live_display.update(
|
|
||||||
Panel(Markdown(full_content), title="[bold engineer]Network Engineer[/bold engineer]", border_style="engineer", expand=False)
|
|
||||||
)
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if response.is_final:
|
if response.is_final:
|
||||||
if live_display:
|
if header_printed:
|
||||||
try: live_display.stop()
|
from rich.rule import Rule
|
||||||
except: pass
|
md_parser.flush()
|
||||||
# Final stop for status to ensure it disappears before the panel
|
|
||||||
if status:
|
if status:
|
||||||
try: status.stop()
|
try: status.stop()
|
||||||
except: pass
|
except: pass
|
||||||
@@ -273,13 +256,14 @@ el.replaceWith(d);
|
|||||||
role_label = "Network Architect" if responder == "architect" else "Network Engineer"
|
role_label = "Network Architect" if responder == "architect" else "Network Engineer"
|
||||||
title = f"[bold {alias}]{role_label}[/bold {alias}]"
|
title = f"[bold {alias}]{role_label}[/bold {alias}]"
|
||||||
|
|
||||||
content_to_print = full_content or final_result.get("response", "")
|
if header_printed:
|
||||||
if content_to_print:
|
from rich.console import Console as RichConsole
|
||||||
if live_display:
|
from ..printer import connpy_theme, get_original_stdout
|
||||||
# Re-render the final frame with correct title/colors
|
stable_console = RichConsole(theme=connpy_theme, file=get_original_stdout())
|
||||||
live_display.update(Panel(Markdown(content_to_print), title=title, border_style=alias, expand=False))
|
stable_console.print(Rule(style=alias))
|
||||||
else:
|
elif not full_content and final_result.get("response"):
|
||||||
printer.console.print(Panel(Markdown(content_to_print), title=title, border_style=alias, expand=False))
|
# If nothing streamed but we have response (e.g. error or direct guide)
|
||||||
|
printer.console.print(Panel(Markdown(final_result["response"]), title=title, border_style=alias, expand=False))
|
||||||
break
|
break
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# Check if it was a gRPC error that we should let handle_errors catch
|
# Check if it was a gRPC error that we should let handle_errors catch
|
||||||
@@ -342,7 +326,6 @@ def ask(self, input_text, dryrun=False, chat_history=None, session_id=None, debu
|
|||||||
import queue
|
import queue
|
||||||
from rich.prompt import Prompt
|
from rich.prompt import Prompt
|
||||||
from rich.text import Text
|
from rich.text import Text
|
||||||
from rich.live import Live
|
|
||||||
from rich.panel import Panel
|
from rich.panel import Panel
|
||||||
from rich.markdown import Markdown
|
from rich.markdown import Markdown
|
||||||
|
|
||||||
@@ -373,7 +356,7 @@ def ask(self, input_text, dryrun=False, chat_history=None, session_id=None, debu
|
|||||||
responses = self.stub.ask(request_generator())
|
responses = self.stub.ask(request_generator())
|
||||||
|
|
||||||
full_content = ""
|
full_content = ""
|
||||||
live_display = None
|
header_printed = False
|
||||||
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
|
||||||
@@ -438,69 +421,53 @@ def ask(self, input_text, dryrun=False, chat_history=None, session_id=None, debu
|
|||||||
|
|
||||||
if response.debug_message:
|
if response.debug_message:
|
||||||
if debug:
|
if debug:
|
||||||
if live_display:
|
|
||||||
try: live_display.stop()
|
|
||||||
except: pass
|
|
||||||
if status:
|
if status:
|
||||||
try: status.stop()
|
try: status.stop()
|
||||||
except: pass
|
except: pass
|
||||||
printer.console.print(Text.from_ansi(response.debug_message))
|
printer.console.print(Text.from_ansi(response.debug_message))
|
||||||
if live_display:
|
if status:
|
||||||
try: live_display.start()
|
|
||||||
except: pass
|
|
||||||
elif status:
|
|
||||||
try: status.start()
|
try: status.start()
|
||||||
except: pass
|
except: pass
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if response.important_message:
|
if response.important_message:
|
||||||
if live_display:
|
|
||||||
try: live_display.stop()
|
|
||||||
except: pass
|
|
||||||
if status:
|
if status:
|
||||||
try: status.stop()
|
try: status.stop()
|
||||||
except: pass
|
except: pass
|
||||||
printer.console.print(Text.from_ansi(response.important_message))
|
printer.console.print(Text.from_ansi(response.important_message))
|
||||||
if live_display:
|
if status:
|
||||||
try: live_display.start()
|
|
||||||
except: pass
|
|
||||||
elif status:
|
|
||||||
try: status.start()
|
try: status.start()
|
||||||
except: pass
|
except: pass
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if not response.is_final:
|
if not response.is_final:
|
||||||
if response.text_chunk:
|
if response.text_chunk:
|
||||||
full_content += response.text_chunk
|
if not header_printed:
|
||||||
|
|
||||||
if not live_display:
|
|
||||||
if status:
|
if status:
|
||||||
try: status.stop()
|
try: status.stop()
|
||||||
except: pass
|
except: pass
|
||||||
|
|
||||||
from rich.console import Console as RichConsole
|
from rich.console import Console as RichConsole
|
||||||
from ..printer import connpy_theme, get_original_stdout
|
from rich.rule import Rule
|
||||||
|
from ..printer import connpy_theme, get_original_stdout, IncrementalMarkdownParser
|
||||||
stable_console = RichConsole(theme=connpy_theme, file=get_original_stdout())
|
stable_console = RichConsole(theme=connpy_theme, file=get_original_stdout())
|
||||||
|
|
||||||
# We default to Engineer title during stream, final result will correct it if needed
|
# Print header on first chunk
|
||||||
live_display = Live(
|
stable_console.print(Rule("[bold engineer]Network Engineer[/bold engineer]", style="engineer"))
|
||||||
Panel(Markdown(full_content), title="[bold engineer]Network Engineer[/bold engineer]", border_style="engineer", expand=False),
|
header_printed = True
|
||||||
console=stable_console,
|
|
||||||
refresh_per_second=8,
|
# Initialize parser
|
||||||
transient=False
|
md_parser = IncrementalMarkdownParser(console=stable_console)
|
||||||
)
|
|
||||||
live_display.start()
|
full_content += response.text_chunk
|
||||||
else:
|
md_parser.feed(response.text_chunk)
|
||||||
live_display.update(
|
|
||||||
Panel(Markdown(full_content), title="[bold engineer]Network Engineer[/bold engineer]", border_style="engineer", expand=False)
|
|
||||||
)
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if response.is_final:
|
if response.is_final:
|
||||||
if live_display:
|
if header_printed:
|
||||||
try: live_display.stop()
|
from rich.rule import Rule
|
||||||
except: pass
|
md_parser.flush()
|
||||||
# Final stop for status to ensure it disappears before the panel
|
|
||||||
if status:
|
if status:
|
||||||
try: status.stop()
|
try: status.stop()
|
||||||
except: pass
|
except: pass
|
||||||
@@ -511,13 +478,14 @@ def ask(self, input_text, dryrun=False, chat_history=None, session_id=None, debu
|
|||||||
role_label = "Network Architect" if responder == "architect" else "Network Engineer"
|
role_label = "Network Architect" if responder == "architect" else "Network Engineer"
|
||||||
title = f"[bold {alias}]{role_label}[/bold {alias}]"
|
title = f"[bold {alias}]{role_label}[/bold {alias}]"
|
||||||
|
|
||||||
content_to_print = full_content or final_result.get("response", "")
|
if header_printed:
|
||||||
if content_to_print:
|
from rich.console import Console as RichConsole
|
||||||
if live_display:
|
from ..printer import connpy_theme, get_original_stdout
|
||||||
# Re-render the final frame with correct title/colors
|
stable_console = RichConsole(theme=connpy_theme, file=get_original_stdout())
|
||||||
live_display.update(Panel(Markdown(content_to_print), title=title, border_style=alias, expand=False))
|
stable_console.print(Rule(style=alias))
|
||||||
else:
|
elif not full_content and final_result.get("response"):
|
||||||
printer.console.print(Panel(Markdown(content_to_print), title=title, border_style=alias, expand=False))
|
# If nothing streamed but we have response (e.g. error or direct guide)
|
||||||
|
printer.console.print(Panel(Markdown(final_result["response"]), title=title, border_style=alias, expand=False))
|
||||||
break
|
break
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# Check if it was a gRPC error that we should let handle_errors catch
|
# Check if it was a gRPC error that we should let handle_errors catch
|
||||||
@@ -1024,7 +992,7 @@ def set_reserved_names(self, names):
|
|||||||
self.remote_host = remote_host
|
self.remote_host = remote_host
|
||||||
self.config = config
|
self.config = config
|
||||||
|
|
||||||
def _handle_remote_copilot(self, res, request_queue, response_queue, client_buffer_bytes, cmd_byte_positions, pause_generator, resume_generator, old_tty):
|
def _handle_remote_copilot(self, res, request_queue, response_queue, client_buffer_bytes, pause_generator, resume_generator, old_tty):
|
||||||
import json, asyncio, termios, sys, tty, queue
|
import json, asyncio, termios, sys, tty, queue
|
||||||
from ..core import copilot_terminal_mode
|
from ..core import copilot_terminal_mode
|
||||||
from . import connpy_pb2
|
from . import connpy_pb2
|
||||||
@@ -1032,6 +1000,10 @@ def set_reserved_names(self, names):
|
|||||||
pause_generator()
|
pause_generator()
|
||||||
|
|
||||||
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_tty)
|
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_tty)
|
||||||
|
|
||||||
|
node_info = json.loads(res.copilot_node_info_json) if res.copilot_node_info_json else {}
|
||||||
|
blocks = node_info.get("context_blocks", [])
|
||||||
|
|
||||||
interface = CopilotInterface(
|
interface = CopilotInterface(
|
||||||
self.config,
|
self.config,
|
||||||
history=getattr(self, 'copilot_history', None),
|
history=getattr(self, 'copilot_history', None),
|
||||||
@@ -1039,8 +1011,6 @@ def set_reserved_names(self, names):
|
|||||||
)
|
)
|
||||||
self.copilot_history = interface.history
|
self.copilot_history = interface.history
|
||||||
self.copilot_state = interface.session_state
|
self.copilot_state = interface.session_state
|
||||||
|
|
||||||
node_info = json.loads(res.copilot_node_info_json) if res.copilot_node_info_json else {}
|
|
||||||
|
|
||||||
async def on_ai_call_remote(active_buffer, question, chunk_callback, merged_node_info):
|
async def on_ai_call_remote(active_buffer, question, chunk_callback, merged_node_info):
|
||||||
# Send request to server
|
# Send request to server
|
||||||
@@ -1066,9 +1036,9 @@ def set_reserved_names(self, names):
|
|||||||
while True:
|
while True:
|
||||||
action, commands, custom_cmd = await interface.run_session(
|
action, commands, custom_cmd = await interface.run_session(
|
||||||
raw_bytes=bytes(client_buffer_bytes),
|
raw_bytes=bytes(client_buffer_bytes),
|
||||||
cmd_byte_positions=cmd_byte_positions,
|
|
||||||
node_info=node_info,
|
node_info=node_info,
|
||||||
on_ai_call=on_ai_call_remote
|
on_ai_call=on_ai_call_remote,
|
||||||
|
blocks=blocks
|
||||||
)
|
)
|
||||||
|
|
||||||
if action == "continue":
|
if action == "continue":
|
||||||
@@ -1081,6 +1051,7 @@ def set_reserved_names(self, names):
|
|||||||
with copilot_terminal_mode():
|
with copilot_terminal_mode():
|
||||||
action, commands, custom_cmd = asyncio.run(run_remote_copilot())
|
action, commands, custom_cmd = asyncio.run(run_remote_copilot())
|
||||||
|
|
||||||
|
print("\033[2m Returning to session...\033[0m", flush=True)
|
||||||
# Prepare final action for server
|
# Prepare final action for server
|
||||||
action_sent = "cancel"
|
action_sent = "cancel"
|
||||||
if action == "send_all" and commands:
|
if action == "send_all" and commands:
|
||||||
@@ -1105,7 +1076,6 @@ def set_reserved_names(self, names):
|
|||||||
|
|
||||||
request_queue = queue.Queue()
|
request_queue = queue.Queue()
|
||||||
client_buffer_bytes = bytearray()
|
client_buffer_bytes = bytearray()
|
||||||
cmd_byte_positions = [(0, None)]
|
|
||||||
pause_stdin = [False]
|
pause_stdin = [False]
|
||||||
wake_r, wake_w = os.pipe()
|
wake_r, wake_w = os.pipe()
|
||||||
|
|
||||||
@@ -1152,8 +1122,6 @@ def set_reserved_names(self, names):
|
|||||||
data = os.read(sys.stdin.fileno(), 1024)
|
data = os.read(sys.stdin.fileno(), 1024)
|
||||||
if not data:
|
if not data:
|
||||||
break
|
break
|
||||||
if b'\r' in data or b'\n' in data:
|
|
||||||
cmd_byte_positions.append((len(client_buffer_bytes), None))
|
|
||||||
yield connpy_pb2.InteractRequest(stdin_data=data)
|
yield connpy_pb2.InteractRequest(stdin_data=data)
|
||||||
except OSError:
|
except OSError:
|
||||||
break
|
break
|
||||||
@@ -1227,14 +1195,11 @@ def set_reserved_names(self, names):
|
|||||||
if res.copilot_prompt:
|
if res.copilot_prompt:
|
||||||
self._handle_remote_copilot(
|
self._handle_remote_copilot(
|
||||||
res, request_queue, response_queue,
|
res, request_queue, response_queue,
|
||||||
client_buffer_bytes, cmd_byte_positions,
|
client_buffer_bytes,
|
||||||
pause_generator, resume_generator, old_tty
|
pause_generator, resume_generator, old_tty
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if res.copilot_injected_command:
|
|
||||||
cmd_byte_positions.append((len(client_buffer_bytes), res.copilot_injected_command))
|
|
||||||
|
|
||||||
if res.stdout_data:
|
if res.stdout_data:
|
||||||
os.write(sys.stdout.fileno(), res.stdout_data)
|
os.write(sys.stdout.fileno(), res.stdout_data)
|
||||||
client_buffer_bytes.extend(res.stdout_data)
|
client_buffer_bytes.extend(res.stdout_data)
|
||||||
@@ -1256,7 +1221,6 @@ def set_reserved_names(self, names):
|
|||||||
params_json = json.dumps(connection_params)
|
params_json = json.dumps(connection_params)
|
||||||
request_queue = queue.Queue()
|
request_queue = queue.Queue()
|
||||||
client_buffer_bytes = bytearray()
|
client_buffer_bytes = bytearray()
|
||||||
cmd_byte_positions = [(0, None)]
|
|
||||||
pause_stdin = [False]
|
pause_stdin = [False]
|
||||||
wake_r, wake_w = os.pipe()
|
wake_r, wake_w = os.pipe()
|
||||||
|
|
||||||
@@ -1304,8 +1268,6 @@ def set_reserved_names(self, names):
|
|||||||
data = os.read(sys.stdin.fileno(), 1024)
|
data = os.read(sys.stdin.fileno(), 1024)
|
||||||
if not data:
|
if not data:
|
||||||
break
|
break
|
||||||
if b'\r' in data or b'\n' in data:
|
|
||||||
cmd_byte_positions.append((len(client_buffer_bytes), None))
|
|
||||||
yield connpy_pb2.InteractRequest(stdin_data=data)
|
yield connpy_pb2.InteractRequest(stdin_data=data)
|
||||||
except OSError:
|
except OSError:
|
||||||
break
|
break
|
||||||
@@ -1378,14 +1340,11 @@ def set_reserved_names(self, names):
|
|||||||
if res.copilot_prompt:
|
if res.copilot_prompt:
|
||||||
self._handle_remote_copilot(
|
self._handle_remote_copilot(
|
||||||
res, request_queue, response_queue,
|
res, request_queue, response_queue,
|
||||||
client_buffer_bytes, cmd_byte_positions,
|
client_buffer_bytes,
|
||||||
pause_generator, resume_generator, old_tty
|
pause_generator, resume_generator, old_tty
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if res.copilot_injected_command:
|
|
||||||
cmd_byte_positions.append((len(client_buffer_bytes), res.copilot_injected_command))
|
|
||||||
|
|
||||||
if res.stdout_data:
|
if res.stdout_data:
|
||||||
os.write(sys.stdout.fileno(), res.stdout_data)
|
os.write(sys.stdout.fileno(), res.stdout_data)
|
||||||
client_buffer_bytes.extend(res.stdout_data)
|
client_buffer_bytes.extend(res.stdout_data)
|
||||||
@@ -1553,7 +1512,6 @@ def connect_dynamic(self, connection_params, debug=False):
|
|||||||
params_json = json.dumps(connection_params)
|
params_json = json.dumps(connection_params)
|
||||||
request_queue = queue.Queue()
|
request_queue = queue.Queue()
|
||||||
client_buffer_bytes = bytearray()
|
client_buffer_bytes = bytearray()
|
||||||
cmd_byte_positions = [(0, None)]
|
|
||||||
pause_stdin = [False]
|
pause_stdin = [False]
|
||||||
wake_r, wake_w = os.pipe()
|
wake_r, wake_w = os.pipe()
|
||||||
|
|
||||||
@@ -1601,8 +1559,6 @@ def connect_dynamic(self, connection_params, debug=False):
|
|||||||
data = os.read(sys.stdin.fileno(), 1024)
|
data = os.read(sys.stdin.fileno(), 1024)
|
||||||
if not data:
|
if not data:
|
||||||
break
|
break
|
||||||
if b'\r' in data or b'\n' in data:
|
|
||||||
cmd_byte_positions.append((len(client_buffer_bytes), None))
|
|
||||||
yield connpy_pb2.InteractRequest(stdin_data=data)
|
yield connpy_pb2.InteractRequest(stdin_data=data)
|
||||||
except OSError:
|
except OSError:
|
||||||
break
|
break
|
||||||
@@ -1675,14 +1631,11 @@ def connect_dynamic(self, connection_params, debug=False):
|
|||||||
if res.copilot_prompt:
|
if res.copilot_prompt:
|
||||||
self._handle_remote_copilot(
|
self._handle_remote_copilot(
|
||||||
res, request_queue, response_queue,
|
res, request_queue, response_queue,
|
||||||
client_buffer_bytes, cmd_byte_positions,
|
client_buffer_bytes,
|
||||||
pause_generator, resume_generator, old_tty
|
pause_generator, resume_generator, old_tty
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if res.copilot_injected_command:
|
|
||||||
cmd_byte_positions.append((len(client_buffer_bytes), res.copilot_injected_command))
|
|
||||||
|
|
||||||
if res.stdout_data:
|
if res.stdout_data:
|
||||||
os.write(sys.stdout.fileno(), res.stdout_data)
|
os.write(sys.stdout.fileno(), res.stdout_data)
|
||||||
client_buffer_bytes.extend(res.stdout_data)
|
client_buffer_bytes.extend(res.stdout_data)
|
||||||
@@ -1713,7 +1666,6 @@ def connect_node(self, unique_id, sftp=False, debug=False, logger=None):
|
|||||||
|
|
||||||
request_queue = queue.Queue()
|
request_queue = queue.Queue()
|
||||||
client_buffer_bytes = bytearray()
|
client_buffer_bytes = bytearray()
|
||||||
cmd_byte_positions = [(0, None)]
|
|
||||||
pause_stdin = [False]
|
pause_stdin = [False]
|
||||||
wake_r, wake_w = os.pipe()
|
wake_r, wake_w = os.pipe()
|
||||||
|
|
||||||
@@ -1760,8 +1712,6 @@ def connect_node(self, unique_id, sftp=False, debug=False, logger=None):
|
|||||||
data = os.read(sys.stdin.fileno(), 1024)
|
data = os.read(sys.stdin.fileno(), 1024)
|
||||||
if not data:
|
if not data:
|
||||||
break
|
break
|
||||||
if b'\r' in data or b'\n' in data:
|
|
||||||
cmd_byte_positions.append((len(client_buffer_bytes), None))
|
|
||||||
yield connpy_pb2.InteractRequest(stdin_data=data)
|
yield connpy_pb2.InteractRequest(stdin_data=data)
|
||||||
except OSError:
|
except OSError:
|
||||||
break
|
break
|
||||||
@@ -1835,14 +1785,11 @@ def connect_node(self, unique_id, sftp=False, debug=False, logger=None):
|
|||||||
if res.copilot_prompt:
|
if res.copilot_prompt:
|
||||||
self._handle_remote_copilot(
|
self._handle_remote_copilot(
|
||||||
res, request_queue, response_queue,
|
res, request_queue, response_queue,
|
||||||
client_buffer_bytes, cmd_byte_positions,
|
client_buffer_bytes,
|
||||||
pause_generator, resume_generator, old_tty
|
pause_generator, resume_generator, old_tty
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if res.copilot_injected_command:
|
|
||||||
cmd_byte_positions.append((len(client_buffer_bytes), res.copilot_injected_command))
|
|
||||||
|
|
||||||
if res.stdout_data:
|
if res.stdout_data:
|
||||||
os.write(sys.stdout.fileno(), res.stdout_data)
|
os.write(sys.stdout.fileno(), res.stdout_data)
|
||||||
client_buffer_bytes.extend(res.stdout_data)
|
client_buffer_bytes.extend(res.stdout_data)
|
||||||
|
|||||||
+152
-70
@@ -737,6 +737,7 @@ class ai:
|
|||||||
- COMPLETE MISSIONS: Execute ALL steps of a mission before reporting back.
|
- COMPLETE MISSIONS: Execute ALL steps of a mission before reporting back.
|
||||||
- DIAGRAM: Use ASCII art or Unicode box-drawing characters directly in your responses to visualize topologies or paths when helpful.
|
- DIAGRAM: Use ASCII art or Unicode box-drawing characters directly in your responses to visualize topologies or paths when helpful.
|
||||||
- EVIDENCE: Include 'Key Snippets' from tool outputs. Be token-efficient.
|
- EVIDENCE: Include 'Key Snippets' from tool outputs. Be token-efficient.
|
||||||
|
- LANGUAGE: You MUST respond in the same language used by the user in their question or instruction.
|
||||||
- NO WANDERING: Do not speculate. If stuck, report attempts.
|
- NO WANDERING: Do not speculate. If stuck, report attempts.
|
||||||
- SAFETY: When you use 'run_commands' with configuration commands, the system automatically prompts the user for confirmation. Just execute - don't ask permission first.
|
- SAFETY: When you use 'run_commands' with configuration commands, the system automatically prompts the user for confirmation. Just execute - don't ask permission first.
|
||||||
{architect_instructions}
|
{architect_instructions}
|
||||||
@@ -754,6 +755,7 @@ class ai:
|
|||||||
- ENGINEER CAPABILITIES: Your Engineer can:
|
- ENGINEER CAPABILITIES: Your Engineer can:
|
||||||
* Filter nodes (list_nodes), Run CLI commands (run_commands), Get metadata (get_node_info).
|
* Filter nodes (list_nodes), Run CLI commands (run_commands), Get metadata (get_node_info).
|
||||||
- ANALYSIS: Review technical findings to identify patterns or design failures.
|
- ANALYSIS: Review technical findings to identify patterns or design failures.
|
||||||
|
- LANGUAGE: You MUST respond in the same language used by the user in their question or instruction.
|
||||||
- MEMORY: Update long-term facts ONLY when the user explicitly requests it.
|
- MEMORY: Update long-term facts ONLY when the user explicitly requests it.
|
||||||
|
|
||||||
CRITICAL - EFFICIENT DELEGATION:
|
CRITICAL - EFFICIENT DELEGATION:
|
||||||
@@ -829,7 +831,6 @@ class ai:
|
|||||||
- 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
|
||||||
"""
|
"""
|
||||||
from rich.live import Live
|
|
||||||
|
|
||||||
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, api_key=api_key, stream=True, **kwargs)
|
||||||
|
|
||||||
@@ -837,7 +838,7 @@ class ai:
|
|||||||
full_content = ""
|
full_content = ""
|
||||||
is_streaming_text = False
|
is_streaming_text = False
|
||||||
has_tool_calls = False
|
has_tool_calls = False
|
||||||
live_display = None
|
header_printed = False
|
||||||
|
|
||||||
# Determine styling based on current brain
|
# Determine styling based on current brain
|
||||||
role_label = "Network Architect" if "architect" in label.lower() else "Network Engineer"
|
role_label = "Network Architect" if "architect" in label.lower() else "Network Engineer"
|
||||||
@@ -866,7 +867,6 @@ class ai:
|
|||||||
|
|
||||||
if not chunk_callback:
|
if not chunk_callback:
|
||||||
if not is_streaming_text:
|
if not is_streaming_text:
|
||||||
# Stop spinner definitively
|
|
||||||
if status:
|
if status:
|
||||||
try:
|
try:
|
||||||
status.stop()
|
status.stop()
|
||||||
@@ -875,35 +875,28 @@ class ai:
|
|||||||
|
|
||||||
# Create a stable, direct Console to bypass _ConsoleProxy recreation bugs
|
# Create a stable, direct Console to bypass _ConsoleProxy recreation bugs
|
||||||
from rich.console import Console as RichConsole
|
from rich.console import Console as RichConsole
|
||||||
from .printer import connpy_theme, get_original_stdout
|
from rich.rule import Rule
|
||||||
|
from .printer import connpy_theme, get_original_stdout, IncrementalMarkdownParser
|
||||||
stable_console = RichConsole(theme=connpy_theme, file=get_original_stdout())
|
stable_console = RichConsole(theme=connpy_theme, file=get_original_stdout())
|
||||||
|
|
||||||
live_display = Live(
|
stable_console.print(Rule(f"[bold {border}]{title}[/bold {border}]", style=border))
|
||||||
Panel(Markdown(full_content), title=title, border_style=border, expand=False),
|
header_printed = True
|
||||||
console=stable_console,
|
md_parser = IncrementalMarkdownParser(console=stable_console)
|
||||||
refresh_per_second=8,
|
|
||||||
transient=False
|
|
||||||
)
|
|
||||||
live_display.start()
|
|
||||||
is_streaming_text = True
|
is_streaming_text = True
|
||||||
else:
|
|
||||||
live_display.update(
|
md_parser.feed(delta.content)
|
||||||
Panel(Markdown(full_content), title=title, border_style=border, expand=False)
|
|
||||||
)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if not chunks:
|
if not chunks:
|
||||||
raise
|
raise
|
||||||
finally:
|
finally:
|
||||||
if live_display:
|
if header_printed:
|
||||||
# Render final state with complete content
|
|
||||||
try:
|
try:
|
||||||
live_display.update(
|
md_parser.flush()
|
||||||
Panel(Markdown(full_content), title=title, border_style=border, expand=False)
|
from rich.console import Console as RichConsole
|
||||||
)
|
from rich.rule import Rule
|
||||||
except Exception:
|
from .printer import connpy_theme, get_original_stdout
|
||||||
pass
|
stable_console = RichConsole(theme=connpy_theme, file=get_original_stdout())
|
||||||
try:
|
stable_console.print(Rule(style=border))
|
||||||
live_display.stop()
|
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -1852,11 +1845,13 @@ class ai:
|
|||||||
if persona == "architect":
|
if persona == "architect":
|
||||||
system_prompt = f"""Role: NETWORK ARCHITECT. You act as a senior strategic advisor during a live SSH session.
|
system_prompt = f"""Role: NETWORK ARCHITECT. You act as a senior strategic advisor during a live SSH session.
|
||||||
Rules:
|
Rules:
|
||||||
1. Answer the user's question directly based on the Terminal Context.
|
1. MANDATORY: You MUST respond in the same language used by the user in their question.
|
||||||
2. Focus on the "why" and "how". Analyze topologies, design patterns, and validate configurations.
|
2. Answer the user's question directly and EXCLUSIVELY based on the Terminal Context.
|
||||||
3. Do NOT provide commands to execute unless specifically requested. Instead, explain the consequences and best practices.
|
3. NO HALLUCINATIONS. The Terminal Context is a live buffer. If it contains only a shell prompt (like 'iol#' or 'admin@vrouter>') and no command output, it means YOU DON'T HAVE DATA. In this case, YOU MUST NOT invent any information.
|
||||||
4. Keep your guide concise and authoritative.
|
4. Focus on the "why" and "how". Analyze topologies, design patterns, and validate configurations.
|
||||||
5. You MUST output your response in the following strict format:
|
5. Do NOT provide commands to execute unless specifically requested. Instead, explain the consequences and best practices.
|
||||||
|
6. Keep your guide concise and authoritative.
|
||||||
|
7. You MUST output your response in the following strict format:
|
||||||
<guide>
|
<guide>
|
||||||
Your brief tactical guide in markdown.
|
Your brief tactical guide in markdown.
|
||||||
</guide>
|
</guide>
|
||||||
@@ -1865,7 +1860,7 @@ Your brief tactical guide in markdown.
|
|||||||
<risk>
|
<risk>
|
||||||
low
|
low
|
||||||
</risk>
|
</risk>
|
||||||
6. Risk level is usually "low" for read-only/no commands.
|
8. Risk level is usually "low" for read-only/no commands.
|
||||||
|
|
||||||
Terminal Context:
|
Terminal Context:
|
||||||
{terminal_buffer}
|
{terminal_buffer}
|
||||||
@@ -1875,11 +1870,13 @@ Node: {node_name}"""
|
|||||||
else:
|
else:
|
||||||
system_prompt = f"""Role: TERMINAL COPILOT. You assist a network engineer during a live SSH session.
|
system_prompt = f"""Role: TERMINAL COPILOT. You assist a network engineer during a live SSH session.
|
||||||
Rules:
|
Rules:
|
||||||
1. Answer the user's question directly based on the Terminal Context.
|
1. MANDATORY: You MUST respond in the same language used by the user in their question.
|
||||||
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.
|
2. EXTREMELY IMPORTANT: Answer EXCLUSIVELY based on the provided Terminal Context.
|
||||||
3. If the user wants to execute an action, provide the required CLI commands inside a <commands> block, one command per line. If no commands are needed, leave it empty or omit the block.
|
3. NO HALLUCINATIONS. The Terminal Context is a live buffer. If it contains only a shell prompt (like 'iol#' or 'admin@vrouter>') and no command output, it means YOU DON'T HAVE DATA. In this case, YOU MUST NOT invent any information. Instead, explicitly state that you don't see the data and offer the correct CLI commands to retrieve it.
|
||||||
4. ULTRA-CONCISE. Keep your guide to the point.
|
4. 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.
|
||||||
5. You MUST output your response in the following strict format:
|
5. If the user wants to execute an action, provide the required CLI commands inside a <commands> block, one command per line. If no commands are needed, leave it empty or omit the block.
|
||||||
|
6. ULTRA-CONCISE. Keep your guide to the point.
|
||||||
|
7. You MUST output your response in the following strict format:
|
||||||
<guide>
|
<guide>
|
||||||
Your brief tactical guide in markdown. 3-4 sentences max.
|
Your brief tactical guide in markdown. 3-4 sentences max.
|
||||||
</guide>
|
</guide>
|
||||||
@@ -1890,7 +1887,7 @@ command 2
|
|||||||
<risk>
|
<risk>
|
||||||
low, high, or destructive
|
low, high, or destructive
|
||||||
</risk>
|
</risk>
|
||||||
6. Risk level: "low" for read-only/no commands, "high" for config changes, "destructive" for potentially dangerous ops.
|
8. Risk level: "low" for read-only/no commands, "high" for config changes, "destructive" for potentially dangerous ops.
|
||||||
|
|
||||||
Terminal Context:
|
Terminal Context:
|
||||||
{terminal_buffer}
|
{terminal_buffer}
|
||||||
@@ -1932,6 +1929,7 @@ Node: {node_name}"""
|
|||||||
try:
|
try:
|
||||||
while iteration < max_iterations:
|
while iteration < max_iterations:
|
||||||
iteration += 1
|
iteration += 1
|
||||||
|
|
||||||
response = await acompletion(
|
response = await acompletion(
|
||||||
model=current_model,
|
model=current_model,
|
||||||
messages=messages,
|
messages=messages,
|
||||||
@@ -2177,11 +2175,13 @@ def engineer_system_prompt(self):
|
|||||||
if persona == "architect":
|
if persona == "architect":
|
||||||
system_prompt = f"""Role: NETWORK ARCHITECT. You act as a senior strategic advisor during a live SSH session.
|
system_prompt = f"""Role: NETWORK ARCHITECT. You act as a senior strategic advisor during a live SSH session.
|
||||||
Rules:
|
Rules:
|
||||||
1. Answer the user's question directly based on the Terminal Context.
|
1. MANDATORY: You MUST respond in the same language used by the user in their question.
|
||||||
2. Focus on the "why" and "how". Analyze topologies, design patterns, and validate configurations.
|
2. Answer the user's question directly and EXCLUSIVELY based on the Terminal Context.
|
||||||
3. Do NOT provide commands to execute unless specifically requested. Instead, explain the consequences and best practices.
|
3. NO HALLUCINATIONS. The Terminal Context is a live buffer. If it contains only a shell prompt (like 'iol#' or 'admin@vrouter>') and no command output, it means YOU DON'T HAVE DATA. In this case, YOU MUST NOT invent any information.
|
||||||
4. Keep your guide concise and authoritative.
|
4. Focus on the "why" and "how". Analyze topologies, design patterns, and validate configurations.
|
||||||
5. You MUST output your response in the following strict format:
|
5. Do NOT provide commands to execute unless specifically requested. Instead, explain the consequences and best practices.
|
||||||
|
6. Keep your guide concise and authoritative.
|
||||||
|
7. You MUST output your response in the following strict format:
|
||||||
<guide>
|
<guide>
|
||||||
Your brief tactical guide in markdown.
|
Your brief tactical guide in markdown.
|
||||||
</guide>
|
</guide>
|
||||||
@@ -2190,7 +2190,7 @@ Your brief tactical guide in markdown.
|
|||||||
<risk>
|
<risk>
|
||||||
low
|
low
|
||||||
</risk>
|
</risk>
|
||||||
6. Risk level is usually "low" for read-only/no commands.
|
8. Risk level is usually "low" for read-only/no commands.
|
||||||
|
|
||||||
Terminal Context:
|
Terminal Context:
|
||||||
{terminal_buffer}
|
{terminal_buffer}
|
||||||
@@ -2200,11 +2200,13 @@ Node: {node_name}"""
|
|||||||
else:
|
else:
|
||||||
system_prompt = f"""Role: TERMINAL COPILOT. You assist a network engineer during a live SSH session.
|
system_prompt = f"""Role: TERMINAL COPILOT. You assist a network engineer during a live SSH session.
|
||||||
Rules:
|
Rules:
|
||||||
1. Answer the user's question directly based on the Terminal Context.
|
1. MANDATORY: You MUST respond in the same language used by the user in their question.
|
||||||
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.
|
2. EXTREMELY IMPORTANT: Answer EXCLUSIVELY based on the provided Terminal Context.
|
||||||
3. If the user wants to execute an action, provide the required CLI commands inside a <commands> block, one command per line. If no commands are needed, leave it empty or omit the block.
|
3. NO HALLUCINATIONS. The Terminal Context is a live buffer. If it contains only a shell prompt (like 'iol#' or 'admin@vrouter>') and no command output, it means YOU DON'T HAVE DATA. In this case, YOU MUST NOT invent any information. Instead, explicitly state that you don't see the data and offer the correct CLI commands to retrieve it.
|
||||||
4. ULTRA-CONCISE. Keep your guide to the point.
|
4. 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.
|
||||||
5. You MUST output your response in the following strict format:
|
5. If the user wants to execute an action, provide the required CLI commands inside a <commands> block, one command per line. If no commands are needed, leave it empty or omit the block.
|
||||||
|
6. ULTRA-CONCISE. Keep your guide to the point.
|
||||||
|
7. You MUST output your response in the following strict format:
|
||||||
<guide>
|
<guide>
|
||||||
Your brief tactical guide in markdown. 3-4 sentences max.
|
Your brief tactical guide in markdown. 3-4 sentences max.
|
||||||
</guide>
|
</guide>
|
||||||
@@ -2215,7 +2217,7 @@ command 2
|
|||||||
<risk>
|
<risk>
|
||||||
low, high, or destructive
|
low, high, or destructive
|
||||||
</risk>
|
</risk>
|
||||||
6. Risk level: "low" for read-only/no commands, "high" for config changes, "destructive" for potentially dangerous ops.
|
8. Risk level: "low" for read-only/no commands, "high" for config changes, "destructive" for potentially dangerous ops.
|
||||||
|
|
||||||
Terminal Context:
|
Terminal Context:
|
||||||
{terminal_buffer}
|
{terminal_buffer}
|
||||||
@@ -2257,6 +2259,7 @@ Node: {node_name}"""
|
|||||||
try:
|
try:
|
||||||
while iteration < max_iterations:
|
while iteration < max_iterations:
|
||||||
iteration += 1
|
iteration += 1
|
||||||
|
|
||||||
response = await acompletion(
|
response = await acompletion(
|
||||||
model=current_model,
|
model=current_model,
|
||||||
messages=messages,
|
messages=messages,
|
||||||
@@ -4119,6 +4122,7 @@ class node:
|
|||||||
self.output = ""
|
self.output = ""
|
||||||
self.status = 1
|
self.status = 1
|
||||||
self.result = {}
|
self.result = {}
|
||||||
|
self.cmd_byte_positions = [(0, None)]
|
||||||
|
|
||||||
@MethodHook
|
@MethodHook
|
||||||
def _passtx(self, passwords, *, keyfile=None):
|
def _passtx(self, passwords, *, keyfile=None):
|
||||||
@@ -4293,9 +4297,9 @@ class node:
|
|||||||
loop = asyncio.get_running_loop()
|
loop = asyncio.get_running_loop()
|
||||||
child_reader_queue = asyncio.Queue()
|
child_reader_queue = asyncio.Queue()
|
||||||
|
|
||||||
# Track command byte positions for copilot context navigation
|
# Reset and track command byte positions for copilot context navigation
|
||||||
# Each entry is (byte_position, command_text_or_None)
|
# Each entry is (byte_position, command_text_or_None)
|
||||||
cmd_byte_positions = [(0, None)]
|
self.cmd_byte_positions = [(self.mylog.tell() if hasattr(self, 'mylog') else 0, None)]
|
||||||
|
|
||||||
def _child_read_ready():
|
def _child_read_ready():
|
||||||
try:
|
try:
|
||||||
@@ -4336,7 +4340,7 @@ class node:
|
|||||||
node_info["prompt"] = to_str(self.tags.get("prompt", r'>$|#$|\$$|>.$|#.$|\$.$'))
|
node_info["prompt"] = to_str(self.tags.get("prompt", r'>$|#$|\$$|>.$|#.$|\$.$'))
|
||||||
|
|
||||||
# Invoke copilot (async callback handles UI)
|
# Invoke copilot (async callback handles UI)
|
||||||
await copilot_handler(self.mylog.getvalue(), node_info, local_stream, child_fd, cmd_byte_positions)
|
await copilot_handler(self.mylog.getvalue(), node_info, local_stream, child_fd, self.cmd_byte_positions)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Remove any stray \x00 bytes and forward normally
|
# Remove any stray \x00 bytes and forward normally
|
||||||
@@ -4344,10 +4348,9 @@ class node:
|
|||||||
if clean_data:
|
if clean_data:
|
||||||
# Track command boundaries when user hits Enter
|
# Track command boundaries when user hits Enter
|
||||||
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):
|
||||||
cmd_byte_positions.append((self.mylog.tell(), None))
|
self.cmd_byte_positions.append((self.mylog.tell(), None))
|
||||||
|
|
||||||
try:
|
try: os.write(child_fd, clean_data)
|
||||||
os.write(child_fd, clean_data)
|
|
||||||
except OSError:
|
except OSError:
|
||||||
break
|
break
|
||||||
self.lastinput = time()
|
self.lastinput = time()
|
||||||
@@ -4469,6 +4472,45 @@ class node:
|
|||||||
finally:
|
finally:
|
||||||
local_stream.teardown()
|
local_stream.teardown()
|
||||||
|
|
||||||
|
@MethodHook
|
||||||
|
async def inject_commands(self, commands, child_fd, on_inject=None):
|
||||||
|
"""
|
||||||
|
Inject a list of commands into the node's PTY.
|
||||||
|
Handles screen_length_command, history tracking and delays.
|
||||||
|
"""
|
||||||
|
if not commands:
|
||||||
|
return
|
||||||
|
|
||||||
|
# 0. Clear line
|
||||||
|
os.write(child_fd, b'\x15')
|
||||||
|
await asyncio.sleep(0.1)
|
||||||
|
|
||||||
|
# 1. Prepare list (prepend screen_length if exists)
|
||||||
|
slc = self.tags.get("screen_length_command") if hasattr(self, 'tags') and isinstance(self.tags, dict) else None
|
||||||
|
|
||||||
|
to_send = list(commands)
|
||||||
|
if slc and slc not in to_send: # avoid duplicates if already there
|
||||||
|
to_send.insert(0, slc)
|
||||||
|
|
||||||
|
# 2. Inject one by one
|
||||||
|
for cmd in to_send:
|
||||||
|
# Register in node's official history (SKIP if it's the administrative screen length command)
|
||||||
|
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
|
||||||
|
self.cmd_byte_positions.append((log_pos, cmd))
|
||||||
|
|
||||||
|
# Write physically to PTY
|
||||||
|
os.write(child_fd, (cmd + "\n").encode())
|
||||||
|
|
||||||
|
# Notify (e.g., for gRPC or logs) - SKIP for administrative SLC
|
||||||
|
if on_inject and cmd != slc:
|
||||||
|
if asyncio.iscoroutinefunction(on_inject):
|
||||||
|
await on_inject(cmd)
|
||||||
|
else:
|
||||||
|
on_inject(cmd)
|
||||||
|
|
||||||
|
# Delay to avoid overwhelming the router
|
||||||
|
await asyncio.sleep(0.8)
|
||||||
|
|
||||||
@MethodHook
|
@MethodHook
|
||||||
def interact(self, debug=False, logger=None):
|
def interact(self, debug=False, logger=None):
|
||||||
@@ -4550,7 +4592,7 @@ class node:
|
|||||||
while True:
|
while True:
|
||||||
action, commands, custom_cmd = await interface.run_session(
|
action, commands, custom_cmd = await interface.run_session(
|
||||||
raw_bytes=raw_bytes,
|
raw_bytes=raw_bytes,
|
||||||
cmd_byte_positions=cmd_byte_positions,
|
cmd_byte_positions=self.cmd_byte_positions,
|
||||||
node_info=node_info,
|
node_info=node_info,
|
||||||
on_ai_call=on_ai_call
|
on_ai_call=on_ai_call
|
||||||
)
|
)
|
||||||
@@ -4558,6 +4600,7 @@ class node:
|
|||||||
continue
|
continue
|
||||||
break
|
break
|
||||||
finally:
|
finally:
|
||||||
|
print("\033[2m Returning to session...\033[0m", flush=True)
|
||||||
# Reiniciar el lector de la terminal para volver al modo interactivo SSH/Telnet
|
# Reiniciar el lector de la terminal para volver al modo interactivo SSH/Telnet
|
||||||
if hasattr(stream, 'start_reading'):
|
if hasattr(stream, 'start_reading'):
|
||||||
stream.start_reading()
|
stream.start_reading()
|
||||||
@@ -4566,20 +4609,7 @@ class node:
|
|||||||
|
|
||||||
if action in ("send_all", "custom"):
|
if action in ("send_all", "custom"):
|
||||||
cmds_to_send = commands if action == "send_all" else custom_cmd
|
cmds_to_send = commands if action == "send_all" else custom_cmd
|
||||||
|
await self.inject_commands(cmds_to_send, child_fd)
|
||||||
if cmds_to_send:
|
|
||||||
os.write(child_fd, b'\x15') # Ctrl+U
|
|
||||||
await asyncio.sleep(0.1)
|
|
||||||
|
|
||||||
# Prepend screen length command to avoid pagination
|
|
||||||
if "screen_length_command" in self.tags:
|
|
||||||
cmds_to_send.insert(0, self.tags["screen_length_command"])
|
|
||||||
|
|
||||||
for cmd in cmds_to_send:
|
|
||||||
if cmd_byte_positions is not None:
|
|
||||||
cmd_byte_positions.append((self.mylog.tell(), cmd))
|
|
||||||
os.write(child_fd, (cmd + "\n").encode())
|
|
||||||
await asyncio.sleep(0.8)
|
|
||||||
else:
|
else:
|
||||||
os.write(child_fd, b'\x15\r')
|
os.write(child_fd, b'\x15\r')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -5073,6 +5103,57 @@ class node:
|
|||||||
</code></pre></div>
|
</code></pre></div>
|
||||||
<h3>Methods</h3>
|
<h3>Methods</h3>
|
||||||
<dl>
|
<dl>
|
||||||
|
<dt id="connpy.node.inject_commands"><code class="name flex">
|
||||||
|
<span>async def <span class="ident">inject_commands</span></span>(<span>self, commands, child_fd, on_inject=None)</span>
|
||||||
|
</code></dt>
|
||||||
|
<dd>
|
||||||
|
<details class="source">
|
||||||
|
<summary>
|
||||||
|
<span>Expand source code</span>
|
||||||
|
</summary>
|
||||||
|
<pre><code class="python">@MethodHook
|
||||||
|
async def inject_commands(self, commands, child_fd, on_inject=None):
|
||||||
|
"""
|
||||||
|
Inject a list of commands into the node's PTY.
|
||||||
|
Handles screen_length_command, history tracking and delays.
|
||||||
|
"""
|
||||||
|
if not commands:
|
||||||
|
return
|
||||||
|
|
||||||
|
# 0. Clear line
|
||||||
|
os.write(child_fd, b'\x15')
|
||||||
|
await asyncio.sleep(0.1)
|
||||||
|
|
||||||
|
# 1. Prepare list (prepend screen_length if exists)
|
||||||
|
slc = self.tags.get("screen_length_command") if hasattr(self, 'tags') and isinstance(self.tags, dict) else None
|
||||||
|
|
||||||
|
to_send = list(commands)
|
||||||
|
if slc and slc not in to_send: # avoid duplicates if already there
|
||||||
|
to_send.insert(0, slc)
|
||||||
|
|
||||||
|
# 2. Inject one by one
|
||||||
|
for cmd in to_send:
|
||||||
|
# Register in node's official history (SKIP if it's the administrative screen length command)
|
||||||
|
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
|
||||||
|
self.cmd_byte_positions.append((log_pos, cmd))
|
||||||
|
|
||||||
|
# Write physically to PTY
|
||||||
|
os.write(child_fd, (cmd + "\n").encode())
|
||||||
|
|
||||||
|
# Notify (e.g., for gRPC or logs) - SKIP for administrative SLC
|
||||||
|
if on_inject and cmd != slc:
|
||||||
|
if asyncio.iscoroutinefunction(on_inject):
|
||||||
|
await on_inject(cmd)
|
||||||
|
else:
|
||||||
|
on_inject(cmd)
|
||||||
|
|
||||||
|
# Delay to avoid overwhelming the router
|
||||||
|
await asyncio.sleep(0.8)</code></pre>
|
||||||
|
</details>
|
||||||
|
<div class="desc"><p>Inject a list of commands into the node's PTY.
|
||||||
|
Handles screen_length_command, history tracking and delays.</p></div>
|
||||||
|
</dd>
|
||||||
<dt id="connpy.node.interact"><code class="name flex">
|
<dt id="connpy.node.interact"><code class="name flex">
|
||||||
<span>def <span class="ident">interact</span></span>(<span>self, debug=False, logger=None)</span>
|
<span>def <span class="ident">interact</span></span>(<span>self, debug=False, logger=None)</span>
|
||||||
</code></dt>
|
</code></dt>
|
||||||
@@ -6189,6 +6270,7 @@ def test(self, commands, expected, vars = None,*, folder = None, prompt = None,
|
|||||||
<li>
|
<li>
|
||||||
<h4><code><a title="connpy.node" href="#connpy.node">node</a></code></h4>
|
<h4><code><a title="connpy.node" href="#connpy.node">node</a></code></h4>
|
||||||
<ul class="">
|
<ul class="">
|
||||||
|
<li><code><a title="connpy.node.inject_commands" href="#connpy.node.inject_commands">inject_commands</a></code></li>
|
||||||
<li><code><a title="connpy.node.interact" href="#connpy.node.interact">interact</a></code></li>
|
<li><code><a title="connpy.node.interact" href="#connpy.node.interact">interact</a></code></li>
|
||||||
<li><code><a title="connpy.node.run" href="#connpy.node.run">run</a></code></li>
|
<li><code><a title="connpy.node.run" href="#connpy.node.run">run</a></code></li>
|
||||||
<li><code><a title="connpy.node.test" href="#connpy.node.test">test</a></code></li>
|
<li><code><a title="connpy.node.test" href="#connpy.node.test">test</a></code></li>
|
||||||
|
|||||||
@@ -58,10 +58,10 @@ el.replaceWith(d);
|
|||||||
<pre><code class="python">class AIService(BaseService):
|
<pre><code class="python">class AIService(BaseService):
|
||||||
"""Business logic for interacting with AI agents and LLM configurations."""
|
"""Business logic for interacting with AI agents and LLM configurations."""
|
||||||
|
|
||||||
def build_context_blocks(self, raw_bytes: bytes, cmd_byte_positions: list, node_info: dict) -> 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 = []
|
||||||
if not (cmd_byte_positions and len(cmd_byte_positions) >= 2 and raw_bytes):
|
if not raw_bytes:
|
||||||
return blocks
|
return blocks
|
||||||
|
|
||||||
default_prompt = r'>$|#$|\$$|>.$|#.$|\$.$'
|
default_prompt = r'>$|#$|\$$|>.$|#.$|\$.$'
|
||||||
@@ -72,29 +72,63 @@ el.replaceWith(d);
|
|||||||
except Exception:
|
except Exception:
|
||||||
prompt_re = re.compile(re.sub(r'(?<!\\)\$', '', default_prompt))
|
prompt_re = re.compile(re.sub(r'(?<!\\)\$', '', default_prompt))
|
||||||
|
|
||||||
for i in range(1, len(cmd_byte_positions)):
|
parsed_positions = []
|
||||||
pos, known_cmd = cmd_byte_positions[i]
|
if cmd_byte_positions and len(cmd_byte_positions) >= 1:
|
||||||
prev_pos = cmd_byte_positions[i-1][0]
|
for i in range(1, len(cmd_byte_positions)):
|
||||||
|
pos, known_cmd = cmd_byte_positions[i]
|
||||||
if known_cmd:
|
prev_pos = cmd_byte_positions[i-1][0]
|
||||||
prev_chunk = raw_bytes[prev_pos:pos]
|
|
||||||
prev_cleaned = log_cleaner(prev_chunk.decode(errors='replace'))
|
|
||||||
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
|
|
||||||
blocks.append((pos, preview[:80]))
|
|
||||||
else:
|
|
||||||
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:
|
if known_cmd:
|
||||||
match = prompt_re.search(preview)
|
prev_chunk = raw_bytes[prev_pos:pos]
|
||||||
if match:
|
prev_cleaned = log_cleaner(prev_chunk.decode(errors='replace'))
|
||||||
cmd_text = preview[match.end():].strip()
|
prev_lines = [l for l in prev_cleaned.split('\n') if l.strip()]
|
||||||
if cmd_text:
|
prompt_text = prev_lines[-1].strip() if prev_lines else ""
|
||||||
blocks.append((pos, preview[:80]))
|
preview = f"{prompt_text}{known_cmd}" if prompt_text else known_cmd
|
||||||
|
parsed_positions.append({"pos": pos, "type": "VALID_CMD", "preview": preview[:80]})
|
||||||
|
else:
|
||||||
|
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:
|
||||||
|
match = prompt_re.search(preview)
|
||||||
|
if match:
|
||||||
|
cmd_text = preview[match.end():].strip()
|
||||||
|
if cmd_text:
|
||||||
|
parsed_positions.append({"pos": pos, "type": "VALID_CMD", "preview": preview[:80]})
|
||||||
|
else:
|
||||||
|
parsed_positions.append({"pos": pos, "type": "EMPTY_PROMPT", "preview": ""})
|
||||||
|
else:
|
||||||
|
parsed_positions.append({"pos": pos, "type": "SCROLLING", "preview": ""})
|
||||||
|
else:
|
||||||
|
parsed_positions.append({"pos": pos, "type": "SCROLLING", "preview": ""})
|
||||||
|
|
||||||
|
last_newline = raw_bytes.rfind(b'\n')
|
||||||
|
current_prompt_pos = last_newline + 1 if last_newline != -1 else 0
|
||||||
|
current_end = len(raw_bytes)
|
||||||
|
|
||||||
|
for i, item in enumerate(parsed_positions):
|
||||||
|
if item["type"] == "VALID_CMD":
|
||||||
|
start_pos = item["pos"]
|
||||||
|
preview = item["preview"]
|
||||||
|
|
||||||
|
# Find the end position: next VALID_CMD or EMPTY_PROMPT
|
||||||
|
end_pos = current_prompt_pos
|
||||||
|
for j in range(i + 1, len(parsed_positions)):
|
||||||
|
next_item = parsed_positions[j]
|
||||||
|
if next_item["type"] in ("VALID_CMD", "EMPTY_PROMPT"):
|
||||||
|
end_pos = next_item["pos"]
|
||||||
|
break
|
||||||
|
|
||||||
|
blocks.append((start_pos, end_pos, preview))
|
||||||
|
|
||||||
|
# Always ensure there is a final block representing the current prompt
|
||||||
|
if not blocks:
|
||||||
|
blocks.append((current_prompt_pos, current_end, last_line[:80] if last_line else "CURRENT CONTEXT"))
|
||||||
|
elif blocks[-1][0] < current_prompt_pos:
|
||||||
|
blocks.append((current_prompt_pos, current_end, last_line[:80] if last_line else "CURRENT CONTEXT"))
|
||||||
|
|
||||||
return blocks
|
return blocks
|
||||||
|
|
||||||
def process_copilot_input(self, input_text: str, session_state: dict) -> dict:
|
def process_copilot_input(self, input_text: str, session_state: dict) -> dict:
|
||||||
@@ -317,17 +351,17 @@ el.replaceWith(d);
|
|||||||
<div class="desc"><p>Ask the AI copilot for terminal assistance.</p></div>
|
<div class="desc"><p>Ask the AI copilot for terminal assistance.</p></div>
|
||||||
</dd>
|
</dd>
|
||||||
<dt id="connpy.services.ai_service.AIService.build_context_blocks"><code class="name flex">
|
<dt id="connpy.services.ai_service.AIService.build_context_blocks"><code class="name flex">
|
||||||
<span>def <span class="ident">build_context_blocks</span></span>(<span>self, raw_bytes: bytes, cmd_byte_positions: list, node_info: dict) ‑> list</span>
|
<span>def <span class="ident">build_context_blocks</span></span>(<span>self,<br>raw_bytes: bytes,<br>cmd_byte_positions: list,<br>node_info: dict,<br>last_line: str = '') ‑> list</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 build_context_blocks(self, raw_bytes: bytes, cmd_byte_positions: list, node_info: dict) -> list:
|
<pre><code class="python">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 = []
|
||||||
if not (cmd_byte_positions and len(cmd_byte_positions) >= 2 and raw_bytes):
|
if not raw_bytes:
|
||||||
return blocks
|
return blocks
|
||||||
|
|
||||||
default_prompt = r'>$|#$|\$$|>.$|#.$|\$.$'
|
default_prompt = r'>$|#$|\$$|>.$|#.$|\$.$'
|
||||||
@@ -338,29 +372,63 @@ el.replaceWith(d);
|
|||||||
except Exception:
|
except Exception:
|
||||||
prompt_re = re.compile(re.sub(r'(?<!\\)\$', '', default_prompt))
|
prompt_re = re.compile(re.sub(r'(?<!\\)\$', '', default_prompt))
|
||||||
|
|
||||||
for i in range(1, len(cmd_byte_positions)):
|
parsed_positions = []
|
||||||
pos, known_cmd = cmd_byte_positions[i]
|
if cmd_byte_positions and len(cmd_byte_positions) >= 1:
|
||||||
prev_pos = cmd_byte_positions[i-1][0]
|
for i in range(1, len(cmd_byte_positions)):
|
||||||
|
pos, known_cmd = cmd_byte_positions[i]
|
||||||
if known_cmd:
|
prev_pos = cmd_byte_positions[i-1][0]
|
||||||
prev_chunk = raw_bytes[prev_pos:pos]
|
|
||||||
prev_cleaned = log_cleaner(prev_chunk.decode(errors='replace'))
|
|
||||||
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
|
|
||||||
blocks.append((pos, preview[:80]))
|
|
||||||
else:
|
|
||||||
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:
|
if known_cmd:
|
||||||
match = prompt_re.search(preview)
|
prev_chunk = raw_bytes[prev_pos:pos]
|
||||||
if match:
|
prev_cleaned = log_cleaner(prev_chunk.decode(errors='replace'))
|
||||||
cmd_text = preview[match.end():].strip()
|
prev_lines = [l for l in prev_cleaned.split('\n') if l.strip()]
|
||||||
if cmd_text:
|
prompt_text = prev_lines[-1].strip() if prev_lines else ""
|
||||||
blocks.append((pos, preview[:80]))
|
preview = f"{prompt_text}{known_cmd}" if prompt_text else known_cmd
|
||||||
|
parsed_positions.append({"pos": pos, "type": "VALID_CMD", "preview": preview[:80]})
|
||||||
|
else:
|
||||||
|
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:
|
||||||
|
match = prompt_re.search(preview)
|
||||||
|
if match:
|
||||||
|
cmd_text = preview[match.end():].strip()
|
||||||
|
if cmd_text:
|
||||||
|
parsed_positions.append({"pos": pos, "type": "VALID_CMD", "preview": preview[:80]})
|
||||||
|
else:
|
||||||
|
parsed_positions.append({"pos": pos, "type": "EMPTY_PROMPT", "preview": ""})
|
||||||
|
else:
|
||||||
|
parsed_positions.append({"pos": pos, "type": "SCROLLING", "preview": ""})
|
||||||
|
else:
|
||||||
|
parsed_positions.append({"pos": pos, "type": "SCROLLING", "preview": ""})
|
||||||
|
|
||||||
|
last_newline = raw_bytes.rfind(b'\n')
|
||||||
|
current_prompt_pos = last_newline + 1 if last_newline != -1 else 0
|
||||||
|
current_end = len(raw_bytes)
|
||||||
|
|
||||||
|
for i, item in enumerate(parsed_positions):
|
||||||
|
if item["type"] == "VALID_CMD":
|
||||||
|
start_pos = item["pos"]
|
||||||
|
preview = item["preview"]
|
||||||
|
|
||||||
|
# Find the end position: next VALID_CMD or EMPTY_PROMPT
|
||||||
|
end_pos = current_prompt_pos
|
||||||
|
for j in range(i + 1, len(parsed_positions)):
|
||||||
|
next_item = parsed_positions[j]
|
||||||
|
if next_item["type"] in ("VALID_CMD", "EMPTY_PROMPT"):
|
||||||
|
end_pos = next_item["pos"]
|
||||||
|
break
|
||||||
|
|
||||||
|
blocks.append((start_pos, end_pos, preview))
|
||||||
|
|
||||||
|
# Always ensure there is a final block representing the current prompt
|
||||||
|
if not blocks:
|
||||||
|
blocks.append((current_prompt_pos, current_end, last_line[:80] if last_line else "CURRENT CONTEXT"))
|
||||||
|
elif blocks[-1][0] < current_prompt_pos:
|
||||||
|
blocks.append((current_prompt_pos, current_end, last_line[:80] if last_line else "CURRENT CONTEXT"))
|
||||||
|
|
||||||
return blocks</code></pre>
|
return blocks</code></pre>
|
||||||
</details>
|
</details>
|
||||||
<div class="desc"><p>Identifies command blocks in the terminal history.</p></div>
|
<div class="desc"><p>Identifies command blocks in the terminal history.</p></div>
|
||||||
|
|||||||
+117
-49
@@ -113,10 +113,10 @@ el.replaceWith(d);
|
|||||||
<pre><code class="python">class AIService(BaseService):
|
<pre><code class="python">class AIService(BaseService):
|
||||||
"""Business logic for interacting with AI agents and LLM configurations."""
|
"""Business logic for interacting with AI agents and LLM configurations."""
|
||||||
|
|
||||||
def build_context_blocks(self, raw_bytes: bytes, cmd_byte_positions: list, node_info: dict) -> 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 = []
|
||||||
if not (cmd_byte_positions and len(cmd_byte_positions) >= 2 and raw_bytes):
|
if not raw_bytes:
|
||||||
return blocks
|
return blocks
|
||||||
|
|
||||||
default_prompt = r'>$|#$|\$$|>.$|#.$|\$.$'
|
default_prompt = r'>$|#$|\$$|>.$|#.$|\$.$'
|
||||||
@@ -127,29 +127,63 @@ el.replaceWith(d);
|
|||||||
except Exception:
|
except Exception:
|
||||||
prompt_re = re.compile(re.sub(r'(?<!\\)\$', '', default_prompt))
|
prompt_re = re.compile(re.sub(r'(?<!\\)\$', '', default_prompt))
|
||||||
|
|
||||||
for i in range(1, len(cmd_byte_positions)):
|
parsed_positions = []
|
||||||
pos, known_cmd = cmd_byte_positions[i]
|
if cmd_byte_positions and len(cmd_byte_positions) >= 1:
|
||||||
prev_pos = cmd_byte_positions[i-1][0]
|
for i in range(1, len(cmd_byte_positions)):
|
||||||
|
pos, known_cmd = cmd_byte_positions[i]
|
||||||
if known_cmd:
|
prev_pos = cmd_byte_positions[i-1][0]
|
||||||
prev_chunk = raw_bytes[prev_pos:pos]
|
|
||||||
prev_cleaned = log_cleaner(prev_chunk.decode(errors='replace'))
|
|
||||||
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
|
|
||||||
blocks.append((pos, preview[:80]))
|
|
||||||
else:
|
|
||||||
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:
|
if known_cmd:
|
||||||
match = prompt_re.search(preview)
|
prev_chunk = raw_bytes[prev_pos:pos]
|
||||||
if match:
|
prev_cleaned = log_cleaner(prev_chunk.decode(errors='replace'))
|
||||||
cmd_text = preview[match.end():].strip()
|
prev_lines = [l for l in prev_cleaned.split('\n') if l.strip()]
|
||||||
if cmd_text:
|
prompt_text = prev_lines[-1].strip() if prev_lines else ""
|
||||||
blocks.append((pos, preview[:80]))
|
preview = f"{prompt_text}{known_cmd}" if prompt_text else known_cmd
|
||||||
|
parsed_positions.append({"pos": pos, "type": "VALID_CMD", "preview": preview[:80]})
|
||||||
|
else:
|
||||||
|
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:
|
||||||
|
match = prompt_re.search(preview)
|
||||||
|
if match:
|
||||||
|
cmd_text = preview[match.end():].strip()
|
||||||
|
if cmd_text:
|
||||||
|
parsed_positions.append({"pos": pos, "type": "VALID_CMD", "preview": preview[:80]})
|
||||||
|
else:
|
||||||
|
parsed_positions.append({"pos": pos, "type": "EMPTY_PROMPT", "preview": ""})
|
||||||
|
else:
|
||||||
|
parsed_positions.append({"pos": pos, "type": "SCROLLING", "preview": ""})
|
||||||
|
else:
|
||||||
|
parsed_positions.append({"pos": pos, "type": "SCROLLING", "preview": ""})
|
||||||
|
|
||||||
|
last_newline = raw_bytes.rfind(b'\n')
|
||||||
|
current_prompt_pos = last_newline + 1 if last_newline != -1 else 0
|
||||||
|
current_end = len(raw_bytes)
|
||||||
|
|
||||||
|
for i, item in enumerate(parsed_positions):
|
||||||
|
if item["type"] == "VALID_CMD":
|
||||||
|
start_pos = item["pos"]
|
||||||
|
preview = item["preview"]
|
||||||
|
|
||||||
|
# Find the end position: next VALID_CMD or EMPTY_PROMPT
|
||||||
|
end_pos = current_prompt_pos
|
||||||
|
for j in range(i + 1, len(parsed_positions)):
|
||||||
|
next_item = parsed_positions[j]
|
||||||
|
if next_item["type"] in ("VALID_CMD", "EMPTY_PROMPT"):
|
||||||
|
end_pos = next_item["pos"]
|
||||||
|
break
|
||||||
|
|
||||||
|
blocks.append((start_pos, end_pos, preview))
|
||||||
|
|
||||||
|
# Always ensure there is a final block representing the current prompt
|
||||||
|
if not blocks:
|
||||||
|
blocks.append((current_prompt_pos, current_end, last_line[:80] if last_line else "CURRENT CONTEXT"))
|
||||||
|
elif blocks[-1][0] < current_prompt_pos:
|
||||||
|
blocks.append((current_prompt_pos, current_end, last_line[:80] if last_line else "CURRENT CONTEXT"))
|
||||||
|
|
||||||
return blocks
|
return blocks
|
||||||
|
|
||||||
def process_copilot_input(self, input_text: str, session_state: dict) -> dict:
|
def process_copilot_input(self, input_text: str, session_state: dict) -> dict:
|
||||||
@@ -372,17 +406,17 @@ el.replaceWith(d);
|
|||||||
<div class="desc"><p>Ask the AI copilot for terminal assistance.</p></div>
|
<div class="desc"><p>Ask the AI copilot for terminal assistance.</p></div>
|
||||||
</dd>
|
</dd>
|
||||||
<dt id="connpy.services.AIService.build_context_blocks"><code class="name flex">
|
<dt id="connpy.services.AIService.build_context_blocks"><code class="name flex">
|
||||||
<span>def <span class="ident">build_context_blocks</span></span>(<span>self, raw_bytes: bytes, cmd_byte_positions: list, node_info: dict) ‑> list</span>
|
<span>def <span class="ident">build_context_blocks</span></span>(<span>self,<br>raw_bytes: bytes,<br>cmd_byte_positions: list,<br>node_info: dict,<br>last_line: str = '') ‑> list</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 build_context_blocks(self, raw_bytes: bytes, cmd_byte_positions: list, node_info: dict) -> list:
|
<pre><code class="python">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 = []
|
||||||
if not (cmd_byte_positions and len(cmd_byte_positions) >= 2 and raw_bytes):
|
if not raw_bytes:
|
||||||
return blocks
|
return blocks
|
||||||
|
|
||||||
default_prompt = r'>$|#$|\$$|>.$|#.$|\$.$'
|
default_prompt = r'>$|#$|\$$|>.$|#.$|\$.$'
|
||||||
@@ -393,29 +427,63 @@ el.replaceWith(d);
|
|||||||
except Exception:
|
except Exception:
|
||||||
prompt_re = re.compile(re.sub(r'(?<!\\)\$', '', default_prompt))
|
prompt_re = re.compile(re.sub(r'(?<!\\)\$', '', default_prompt))
|
||||||
|
|
||||||
for i in range(1, len(cmd_byte_positions)):
|
parsed_positions = []
|
||||||
pos, known_cmd = cmd_byte_positions[i]
|
if cmd_byte_positions and len(cmd_byte_positions) >= 1:
|
||||||
prev_pos = cmd_byte_positions[i-1][0]
|
for i in range(1, len(cmd_byte_positions)):
|
||||||
|
pos, known_cmd = cmd_byte_positions[i]
|
||||||
if known_cmd:
|
prev_pos = cmd_byte_positions[i-1][0]
|
||||||
prev_chunk = raw_bytes[prev_pos:pos]
|
|
||||||
prev_cleaned = log_cleaner(prev_chunk.decode(errors='replace'))
|
|
||||||
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
|
|
||||||
blocks.append((pos, preview[:80]))
|
|
||||||
else:
|
|
||||||
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:
|
if known_cmd:
|
||||||
match = prompt_re.search(preview)
|
prev_chunk = raw_bytes[prev_pos:pos]
|
||||||
if match:
|
prev_cleaned = log_cleaner(prev_chunk.decode(errors='replace'))
|
||||||
cmd_text = preview[match.end():].strip()
|
prev_lines = [l for l in prev_cleaned.split('\n') if l.strip()]
|
||||||
if cmd_text:
|
prompt_text = prev_lines[-1].strip() if prev_lines else ""
|
||||||
blocks.append((pos, preview[:80]))
|
preview = f"{prompt_text}{known_cmd}" if prompt_text else known_cmd
|
||||||
|
parsed_positions.append({"pos": pos, "type": "VALID_CMD", "preview": preview[:80]})
|
||||||
|
else:
|
||||||
|
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:
|
||||||
|
match = prompt_re.search(preview)
|
||||||
|
if match:
|
||||||
|
cmd_text = preview[match.end():].strip()
|
||||||
|
if cmd_text:
|
||||||
|
parsed_positions.append({"pos": pos, "type": "VALID_CMD", "preview": preview[:80]})
|
||||||
|
else:
|
||||||
|
parsed_positions.append({"pos": pos, "type": "EMPTY_PROMPT", "preview": ""})
|
||||||
|
else:
|
||||||
|
parsed_positions.append({"pos": pos, "type": "SCROLLING", "preview": ""})
|
||||||
|
else:
|
||||||
|
parsed_positions.append({"pos": pos, "type": "SCROLLING", "preview": ""})
|
||||||
|
|
||||||
|
last_newline = raw_bytes.rfind(b'\n')
|
||||||
|
current_prompt_pos = last_newline + 1 if last_newline != -1 else 0
|
||||||
|
current_end = len(raw_bytes)
|
||||||
|
|
||||||
|
for i, item in enumerate(parsed_positions):
|
||||||
|
if item["type"] == "VALID_CMD":
|
||||||
|
start_pos = item["pos"]
|
||||||
|
preview = item["preview"]
|
||||||
|
|
||||||
|
# Find the end position: next VALID_CMD or EMPTY_PROMPT
|
||||||
|
end_pos = current_prompt_pos
|
||||||
|
for j in range(i + 1, len(parsed_positions)):
|
||||||
|
next_item = parsed_positions[j]
|
||||||
|
if next_item["type"] in ("VALID_CMD", "EMPTY_PROMPT"):
|
||||||
|
end_pos = next_item["pos"]
|
||||||
|
break
|
||||||
|
|
||||||
|
blocks.append((start_pos, end_pos, preview))
|
||||||
|
|
||||||
|
# Always ensure there is a final block representing the current prompt
|
||||||
|
if not blocks:
|
||||||
|
blocks.append((current_prompt_pos, current_end, last_line[:80] if last_line else "CURRENT CONTEXT"))
|
||||||
|
elif blocks[-1][0] < current_prompt_pos:
|
||||||
|
blocks.append((current_prompt_pos, current_end, last_line[:80] if last_line else "CURRENT CONTEXT"))
|
||||||
|
|
||||||
return blocks</code></pre>
|
return blocks</code></pre>
|
||||||
</details>
|
</details>
|
||||||
<div class="desc"><p>Identifies command blocks in the terminal history.</p></div>
|
<div class="desc"><p>Identifies command blocks in the terminal history.</p></div>
|
||||||
|
|||||||
+28
-8
@@ -75,14 +75,34 @@ el.replaceWith(d);
|
|||||||
elif token in ('\b', '\x7f'):
|
elif token in ('\b', '\x7f'):
|
||||||
if cursor > 0:
|
if cursor > 0:
|
||||||
cursor -= 1
|
cursor -= 1
|
||||||
elif token == '\x1B[D': # Left Arrow
|
elif token.startswith('\x1B[') and len(token) >= 3:
|
||||||
if cursor > 0:
|
# Parse CSI: \x1B[ <params> <final_char>
|
||||||
cursor -= 1
|
final = token[-1]
|
||||||
elif token == '\x1B[C': # Right Arrow
|
param_str = token[2:-1]
|
||||||
if cursor < len(buffer):
|
n = int(param_str) if param_str.isdigit() else 1
|
||||||
cursor += 1
|
|
||||||
elif token == '\x1B[K': # Clear to end of line
|
if final == 'D': # CUB – Cursor Back
|
||||||
buffer = buffer[:cursor]
|
cursor = max(0, cursor - n)
|
||||||
|
elif final == 'C': # CUF – Cursor Forward
|
||||||
|
cursor = min(len(buffer), cursor + n)
|
||||||
|
elif final == 'K': # EL – Erase in Line
|
||||||
|
if n == 0 or param_str == '': # Clear to end
|
||||||
|
buffer = buffer[:cursor]
|
||||||
|
elif n == 1: # Clear to start
|
||||||
|
buffer[:cursor] = [' '] * cursor
|
||||||
|
elif n == 2: # Clear entire line
|
||||||
|
buffer = []
|
||||||
|
cursor = 0
|
||||||
|
elif final == 'G': # CHA – Cursor Horizontal Absolute (1-indexed)
|
||||||
|
cursor = max(0, n - 1)
|
||||||
|
# Pad buffer if cursor is beyond current length
|
||||||
|
if cursor > len(buffer):
|
||||||
|
buffer.extend([' '] * (cursor - len(buffer)))
|
||||||
|
elif final == 'P': # DCH – Delete Characters
|
||||||
|
del buffer[cursor:cursor + n]
|
||||||
|
elif final == '@': # ICH – Insert Characters
|
||||||
|
buffer[cursor:cursor] = [' '] * n
|
||||||
|
# All other CSI sequences are silently discarded
|
||||||
elif token.startswith('\x1B'):
|
elif token.startswith('\x1B'):
|
||||||
continue
|
continue
|
||||||
elif len(token) == 1 and ord(token) < 32:
|
elif len(token) == 1 and ord(token) < 32:
|
||||||
|
|||||||
Reference in New Issue
Block a user