feat: integrate knowledge store search and background multiplexer with autonomous monitoring into agent pipeline
This commit is contained in:
+45
-2
@@ -72,8 +72,8 @@ def get_tools_definition():
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "run_command_interactive",
|
||||
"description": "Execute an interactive terminal command that requires user input or displays UI. The command runs in the user's terminal. Returns exit code only.",
|
||||
"name": "start_interactive_session",
|
||||
"description": "Execute an interactive terminal command that requires user input or displays UI. The command runs in a dedicated session and returns a session name.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -83,6 +83,49 @@ def get_tools_definition():
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "send_input_to_session",
|
||||
"description": "Send input to an interactive session.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"session_name": {"type": "string", "description": "The name of the session"},
|
||||
"input_data": {"type": "string", "description": "The input to send to the session"}
|
||||
},
|
||||
"required": ["session_name", "input_data"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "read_session_output",
|
||||
"description": "Read output from an interactive session.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"session_name": {"type": "string", "description": "The name of the session"}
|
||||
},
|
||||
"required": ["session_name"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "close_interactive_session",
|
||||
"description": "Close an interactive session.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"session_name": {"type": "string", "description": "The name of the session"}
|
||||
},
|
||||
"required": ["session_name"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
|
||||
+3
-1
@@ -3,6 +3,8 @@ import subprocess
|
||||
import time
|
||||
import select
|
||||
from pr.multiplexer import create_multiplexer, close_multiplexer, get_multiplexer
|
||||
from pr.tools.interactive_control import start_interactive_session
|
||||
from pr.config import MAX_CONCURRENT_SESSIONS
|
||||
|
||||
_processes = {}
|
||||
|
||||
@@ -95,7 +97,7 @@ def tail_process(pid: int, timeout: int = 30):
|
||||
return {"status": "error", "error": f"Process {pid} not found"}
|
||||
|
||||
|
||||
def run_command(command, timeout=30):
|
||||
def run_command(command, timeout=30, monitored=False):
|
||||
mux_name = None
|
||||
try:
|
||||
process = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
from pr.multiplexer import create_multiplexer, get_multiplexer, close_multiplexer, get_all_multiplexer_states
|
||||
|
||||
def start_interactive_session(command, session_name=None, process_type='generic'):
|
||||
"""
|
||||
Start an interactive session in a dedicated multiplexer.
|
||||
|
||||
Args:
|
||||
command: The command to run (list or string)
|
||||
session_name: Optional name for the session
|
||||
process_type: Type of process (ssh, vim, apt, etc.)
|
||||
|
||||
Returns:
|
||||
session_name: The name of the created session
|
||||
"""
|
||||
name, mux = create_multiplexer(session_name)
|
||||
mux.update_metadata('process_type', process_type)
|
||||
|
||||
# Start the process
|
||||
if isinstance(command, str):
|
||||
command = command.split()
|
||||
|
||||
try:
|
||||
process = subprocess.Popen(
|
||||
command,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
bufsize=1
|
||||
)
|
||||
|
||||
mux.process = process
|
||||
mux.update_metadata('pid', process.pid)
|
||||
|
||||
# Set process type and handler
|
||||
detected_type = detect_process_type(command)
|
||||
mux.set_process_type(detected_type)
|
||||
|
||||
# Start output readers
|
||||
stdout_thread = threading.Thread(target=_read_output, args=(process.stdout, mux.write_stdout), daemon=True)
|
||||
stderr_thread = threading.Thread(target=_read_output, args=(process.stderr, mux.write_stderr), daemon=True)
|
||||
|
||||
stdout_thread.start()
|
||||
stderr_thread.start()
|
||||
|
||||
mux.stdout_thread = stdout_thread
|
||||
mux.stderr_thread = stderr_thread
|
||||
|
||||
return name
|
||||
except Exception as e:
|
||||
close_multiplexer(name)
|
||||
raise e
|
||||
|
||||
def _read_output(stream, write_func):
|
||||
"""Read from a stream and write to multiplexer buffer."""
|
||||
try:
|
||||
for line in iter(stream.readline, ''):
|
||||
if line:
|
||||
write_func(line.rstrip('\n'))
|
||||
except Exception as e:
|
||||
print(f"Error reading output: {e}")
|
||||
|
||||
def send_input_to_session(session_name, input_data):
|
||||
"""
|
||||
Send input to an interactive session.
|
||||
|
||||
Args:
|
||||
session_name: Name of the session
|
||||
input_data: Input string to send
|
||||
"""
|
||||
mux = get_multiplexer(session_name)
|
||||
if not mux:
|
||||
raise ValueError(f"Session {session_name} not found")
|
||||
|
||||
if not hasattr(mux, 'process') or mux.process.poll() is not None:
|
||||
raise ValueError(f"Session {session_name} is not active")
|
||||
|
||||
try:
|
||||
mux.process.stdin.write(input_data + '\n')
|
||||
mux.process.stdin.flush()
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"Failed to send input to session {session_name}: {e}")
|
||||
|
||||
def read_session_output(session_name, lines=None):
|
||||
"""
|
||||
Read output from a session.
|
||||
|
||||
Args:
|
||||
session_name: Name of the session
|
||||
lines: Number of recent lines to return (None for all)
|
||||
|
||||
Returns:
|
||||
dict: {'stdout': str, 'stderr': str}
|
||||
"""
|
||||
mux = get_multiplexer(session_name)
|
||||
if not mux:
|
||||
raise ValueError(f"Session {session_name} not found")
|
||||
|
||||
output = mux.get_all_output()
|
||||
if lines is not None:
|
||||
# Return last N lines
|
||||
stdout_lines = output['stdout'].split('\n')[-lines:] if output['stdout'] else []
|
||||
stderr_lines = output['stderr'].split('\n')[-lines:] if output['stderr'] else []
|
||||
output = {
|
||||
'stdout': '\n'.join(stdout_lines),
|
||||
'stderr': '\n'.join(stderr_lines)
|
||||
}
|
||||
return output
|
||||
|
||||
def list_active_sessions():
|
||||
"""
|
||||
List all active interactive sessions.
|
||||
|
||||
Returns:
|
||||
dict: Session states
|
||||
"""
|
||||
return get_all_multiplexer_states()
|
||||
|
||||
def get_session_status(session_name):
|
||||
"""
|
||||
Get detailed status of a session.
|
||||
|
||||
Args:
|
||||
session_name: Name of the session
|
||||
|
||||
Returns:
|
||||
dict: Session metadata and status
|
||||
"""
|
||||
mux = get_multiplexer(session_name)
|
||||
if not mux:
|
||||
return None
|
||||
|
||||
status = mux.get_metadata()
|
||||
status['is_active'] = hasattr(mux, 'process') and mux.process.poll() is None
|
||||
if status['is_active']:
|
||||
status['pid'] = mux.process.pid
|
||||
status['output_summary'] = {
|
||||
'stdout_lines': len(mux.stdout_buffer),
|
||||
'stderr_lines': len(mux.stderr_buffer)
|
||||
}
|
||||
return status
|
||||
|
||||
def close_interactive_session(session_name):
|
||||
"""
|
||||
Close an interactive session.
|
||||
"""
|
||||
try:
|
||||
mux = get_multiplexer(session_name)
|
||||
if mux:
|
||||
mux.process.kill()
|
||||
close_multiplexer(session_name)
|
||||
return {"status": "success"}
|
||||
except Exception as e:
|
||||
return {"status": "error", "error": str(e)}
|
||||
@@ -0,0 +1,264 @@
|
||||
import re
|
||||
import time
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
class ProcessHandler(ABC):
|
||||
"""Base class for process-specific handlers."""
|
||||
|
||||
def __init__(self, multiplexer):
|
||||
self.multiplexer = multiplexer
|
||||
self.state_machine = {}
|
||||
self.current_state = 'initial'
|
||||
self.prompt_patterns = []
|
||||
self.response_suggestions = {}
|
||||
|
||||
@abstractmethod
|
||||
def get_process_type(self):
|
||||
"""Return the process type this handler manages."""
|
||||
pass
|
||||
|
||||
def update_state(self, output):
|
||||
"""Update internal state based on output."""
|
||||
pass
|
||||
|
||||
def get_prompt_suggestions(self):
|
||||
"""Return suggested responses for current state."""
|
||||
return self.response_suggestions.get(self.current_state, [])
|
||||
|
||||
def is_waiting_for_input(self):
|
||||
"""Check if process appears to be waiting for input."""
|
||||
return self.current_state in ['waiting_confirmation', 'waiting_input']
|
||||
|
||||
class AptHandler(ProcessHandler):
|
||||
"""Handler for apt package manager interactions."""
|
||||
|
||||
def __init__(self, multiplexer):
|
||||
super().__init__(multiplexer)
|
||||
self.state_machine = {
|
||||
'initial': ['running_command'],
|
||||
'running_command': ['waiting_confirmation', 'completed'],
|
||||
'waiting_confirmation': ['confirmed', 'cancelled'],
|
||||
'confirmed': ['installing', 'completed'],
|
||||
'installing': ['completed', 'error'],
|
||||
'completed': [],
|
||||
'error': [],
|
||||
'cancelled': []
|
||||
}
|
||||
self.prompt_patterns = [
|
||||
(r'Do you want to continue\?', 'confirmation'),
|
||||
(r'After this operation.*installed\.', 'size_info'),
|
||||
(r'Need to get.*B of archives\.', 'download_info'),
|
||||
(r'Unpacking.*Configuring', 'configuring'),
|
||||
(r'Setting up', 'setting_up'),
|
||||
(r'E:\s', 'error')
|
||||
]
|
||||
|
||||
def get_process_type(self):
|
||||
return 'apt'
|
||||
|
||||
def update_state(self, output):
|
||||
"""Update state based on apt output patterns."""
|
||||
output_lower = output.lower()
|
||||
|
||||
# Check for completion
|
||||
if 'processing triggers' in output_lower or 'done' in output_lower:
|
||||
self.current_state = 'completed'
|
||||
# Check for confirmation prompts
|
||||
elif 'do you want to continue' in output_lower:
|
||||
self.current_state = 'waiting_confirmation'
|
||||
# Check for installation progress
|
||||
elif 'setting up' in output_lower or 'unpacking' in output_lower:
|
||||
self.current_state = 'installing'
|
||||
# Check for errors
|
||||
elif 'e:' in output_lower or 'error' in output_lower:
|
||||
self.current_state = 'error'
|
||||
|
||||
def get_prompt_suggestions(self):
|
||||
"""Return suggested responses for apt prompts."""
|
||||
suggestions = super().get_prompt_suggestions()
|
||||
if self.current_state == 'waiting_confirmation':
|
||||
suggestions.extend(['y', 'yes', 'n', 'no'])
|
||||
return suggestions
|
||||
|
||||
class VimHandler(ProcessHandler):
|
||||
"""Handler for vim editor interactions."""
|
||||
|
||||
def __init__(self, multiplexer):
|
||||
super().__init__(multiplexer)
|
||||
self.state_machine = {
|
||||
'initial': ['normal_mode', 'insert_mode'],
|
||||
'normal_mode': ['insert_mode', 'command_mode', 'visual_mode'],
|
||||
'insert_mode': ['normal_mode'],
|
||||
'command_mode': ['normal_mode'],
|
||||
'visual_mode': ['normal_mode'],
|
||||
'exiting': []
|
||||
}
|
||||
self.prompt_patterns = [
|
||||
(r'-- INSERT --', 'insert_mode'),
|
||||
(r'-- VISUAL --', 'visual_mode'),
|
||||
(r':', 'command_mode'),
|
||||
(r'Press ENTER', 'waiting_enter'),
|
||||
(r'Saved', 'saved')
|
||||
]
|
||||
self.mode_indicators = {
|
||||
'insert': '-- INSERT --',
|
||||
'visual': '-- VISUAL --',
|
||||
'command': ':'
|
||||
}
|
||||
|
||||
def get_process_type(self):
|
||||
return 'vim'
|
||||
|
||||
def update_state(self, output):
|
||||
"""Update state based on vim mode indicators."""
|
||||
if '-- INSERT --' in output:
|
||||
self.current_state = 'insert_mode'
|
||||
elif '-- VISUAL --' in output:
|
||||
self.current_state = 'visual_mode'
|
||||
elif output.strip().endswith(':'):
|
||||
self.current_state = 'command_mode'
|
||||
elif 'Press ENTER' in output:
|
||||
self.current_state = 'waiting_enter'
|
||||
else:
|
||||
# Default to normal mode if no specific indicators
|
||||
self.current_state = 'normal_mode'
|
||||
|
||||
def get_prompt_suggestions(self):
|
||||
"""Return suggested commands for vim modes."""
|
||||
suggestions = super().get_prompt_suggestions()
|
||||
if self.current_state == 'command_mode':
|
||||
suggestions.extend(['w', 'q', 'wq', 'q!', 'w!'])
|
||||
elif self.current_state == 'normal_mode':
|
||||
suggestions.extend(['i', 'a', 'o', 'dd', ':w', ':q'])
|
||||
elif self.current_state == 'waiting_enter':
|
||||
suggestions.extend(['\n'])
|
||||
return suggestions
|
||||
|
||||
class SSHHandler(ProcessHandler):
|
||||
"""Handler for SSH connection interactions."""
|
||||
|
||||
def __init__(self, multiplexer):
|
||||
super().__init__(multiplexer)
|
||||
self.state_machine = {
|
||||
'initial': ['connecting'],
|
||||
'connecting': ['auth_prompt', 'connected', 'failed'],
|
||||
'auth_prompt': ['connected', 'failed'],
|
||||
'connected': ['shell', 'disconnected'],
|
||||
'shell': ['disconnected'],
|
||||
'failed': [],
|
||||
'disconnected': []
|
||||
}
|
||||
self.prompt_patterns = [
|
||||
(r'password:', 'password_prompt'),
|
||||
(r'yes/no', 'host_key_prompt'),
|
||||
(r'Permission denied', 'auth_failed'),
|
||||
(r'Welcome to', 'connected'),
|
||||
(r'\$', 'shell_prompt'),
|
||||
(r'\#', 'root_shell_prompt'),
|
||||
(r'Connection closed', 'disconnected')
|
||||
]
|
||||
|
||||
def get_process_type(self):
|
||||
return 'ssh'
|
||||
|
||||
def update_state(self, output):
|
||||
"""Update state based on SSH connection output."""
|
||||
output_lower = output.lower()
|
||||
|
||||
if 'permission denied' in output_lower:
|
||||
self.current_state = 'failed'
|
||||
elif 'password:' in output_lower:
|
||||
self.current_state = 'auth_prompt'
|
||||
elif 'yes/no' in output_lower:
|
||||
self.current_state = 'auth_prompt'
|
||||
elif 'welcome to' in output_lower or 'last login' in output_lower:
|
||||
self.current_state = 'connected'
|
||||
elif output.strip().endswith('$') or output.strip().endswith('#'):
|
||||
self.current_state = 'shell'
|
||||
elif 'connection closed' in output_lower:
|
||||
self.current_state = 'disconnected'
|
||||
|
||||
def get_prompt_suggestions(self):
|
||||
"""Return suggested responses for SSH prompts."""
|
||||
suggestions = super().get_prompt_suggestions()
|
||||
if self.current_state == 'auth_prompt':
|
||||
if 'password:' in self.multiplexer.get_all_output()['stdout']:
|
||||
suggestions.extend(['<password>']) # Placeholder for actual password
|
||||
elif 'yes/no' in self.multiplexer.get_all_output()['stdout']:
|
||||
suggestions.extend(['yes', 'no'])
|
||||
return suggestions
|
||||
|
||||
class GenericProcessHandler(ProcessHandler):
|
||||
"""Fallback handler for unknown process types."""
|
||||
|
||||
def __init__(self, multiplexer):
|
||||
super().__init__(multiplexer)
|
||||
self.state_machine = {
|
||||
'initial': ['running'],
|
||||
'running': ['waiting_input', 'completed'],
|
||||
'waiting_input': ['running'],
|
||||
'completed': []
|
||||
}
|
||||
self.prompt_patterns = [
|
||||
(r'\?\s*$', 'waiting_input'), # Lines ending with ?
|
||||
(r'>\s*$', 'waiting_input'), # Lines ending with >
|
||||
(r':\s*$', 'waiting_input'), # Lines ending with :
|
||||
(r'done', 'completed'),
|
||||
(r'finished', 'completed'),
|
||||
(r'exit code', 'completed')
|
||||
]
|
||||
|
||||
def get_process_type(self):
|
||||
return 'generic'
|
||||
|
||||
def update_state(self, output):
|
||||
"""Basic state detection for generic processes."""
|
||||
output_lower = output.lower()
|
||||
|
||||
if any(pattern in output_lower for pattern in ['done', 'finished', 'complete']):
|
||||
self.current_state = 'completed'
|
||||
elif any(output.strip().endswith(char) for char in ['?', '>', ':']):
|
||||
self.current_state = 'waiting_input'
|
||||
else:
|
||||
self.current_state = 'running'
|
||||
|
||||
# Handler registry
|
||||
_handler_classes = {
|
||||
'apt': AptHandler,
|
||||
'vim': VimHandler,
|
||||
'ssh': SSHHandler,
|
||||
'generic': GenericProcessHandler
|
||||
}
|
||||
|
||||
def get_handler_for_process(process_type, multiplexer):
|
||||
"""Get appropriate handler for a process type."""
|
||||
handler_class = _handler_classes.get(process_type, GenericProcessHandler)
|
||||
return handler_class(multiplexer)
|
||||
|
||||
def detect_process_type(command):
|
||||
"""Detect process type from command."""
|
||||
command_str = ' '.join(command) if isinstance(command, list) else command
|
||||
command_lower = command_str.lower()
|
||||
|
||||
if 'apt' in command_lower or 'apt-get' in command_lower:
|
||||
return 'apt'
|
||||
elif 'vim' in command_lower or 'vi ' in command_lower:
|
||||
return 'vim'
|
||||
elif 'ssh' in command_lower:
|
||||
return 'ssh'
|
||||
else:
|
||||
return 'generic'
|
||||
return 'ssh'
|
||||
def detect_process_type(command):
|
||||
"""Detect process type from command."""
|
||||
command_str = ' '.join(command) if isinstance(command, list) else command
|
||||
command_lower = command_str.lower()
|
||||
|
||||
if 'apt' in command_lower or 'apt-get' in command_lower:
|
||||
return 'apt'
|
||||
elif 'vim' in command_lower or 'vi ' in command_lower:
|
||||
return 'vim'
|
||||
elif 'ssh' in command_lower:
|
||||
return 'ssh'
|
||||
else:
|
||||
return 'generic'
|
||||
@@ -0,0 +1,278 @@
|
||||
import re
|
||||
import time
|
||||
from collections import defaultdict
|
||||
|
||||
class PromptDetector:
|
||||
"""Detects various process prompts and manages interaction state."""
|
||||
|
||||
def __init__(self):
|
||||
self.prompt_patterns = self._load_prompt_patterns()
|
||||
self.state_machines = self._load_state_machines()
|
||||
self.session_states = {}
|
||||
self.timeout_configs = {
|
||||
'default': 30, # 30 seconds default timeout
|
||||
'apt': 300, # 5 minutes for apt operations
|
||||
'ssh': 60, # 1 minute for SSH connections
|
||||
'vim': 3600 # 1 hour for vim sessions
|
||||
}
|
||||
|
||||
def _load_prompt_patterns(self):
|
||||
"""Load regex patterns for detecting various prompts."""
|
||||
return {
|
||||
'bash_prompt': [
|
||||
re.compile(r'[\w\-\.]+@[\w\-\.]+:.*[\$#]\s*$'),
|
||||
re.compile(r'\$\s*$'),
|
||||
re.compile(r'#\s*$'),
|
||||
re.compile(r'>\s*$') # Continuation prompt
|
||||
],
|
||||
'confirmation': [
|
||||
re.compile(r'[Yy]/[Nn]', re.IGNORECASE),
|
||||
re.compile(r'[Yy]es/[Nn]o', re.IGNORECASE),
|
||||
re.compile(r'continue\?', re.IGNORECASE),
|
||||
re.compile(r'proceed\?', re.IGNORECASE)
|
||||
],
|
||||
'password': [
|
||||
re.compile(r'password:', re.IGNORECASE),
|
||||
re.compile(r'passphrase:', re.IGNORECASE),
|
||||
re.compile(r'enter password', re.IGNORECASE)
|
||||
],
|
||||
'sudo_password': [
|
||||
re.compile(r'\[sudo\].*password', re.IGNORECASE)
|
||||
],
|
||||
'apt': [
|
||||
re.compile(r'Do you want to continue\?', re.IGNORECASE),
|
||||
re.compile(r'After this operation', re.IGNORECASE),
|
||||
re.compile(r'Need to get', re.IGNORECASE)
|
||||
],
|
||||
'vim': [
|
||||
re.compile(r'-- INSERT --'),
|
||||
re.compile(r'-- VISUAL --'),
|
||||
re.compile(r':'),
|
||||
re.compile(r'Press ENTER', re.IGNORECASE)
|
||||
],
|
||||
'ssh': [
|
||||
re.compile(r'yes/no', re.IGNORECASE),
|
||||
re.compile(r'password:', re.IGNORECASE),
|
||||
re.compile(r'Permission denied', re.IGNORECASE)
|
||||
],
|
||||
'git': [
|
||||
re.compile(r'Username:', re.IGNORECASE),
|
||||
re.compile(r'Email:', re.IGNORECASE)
|
||||
],
|
||||
'error': [
|
||||
re.compile(r'error:', re.IGNORECASE),
|
||||
re.compile(r'failed', re.IGNORECASE),
|
||||
re.compile(r'exception', re.IGNORECASE)
|
||||
]
|
||||
}
|
||||
|
||||
def _load_state_machines(self):
|
||||
"""Load state machines for different process types."""
|
||||
return {
|
||||
'apt': {
|
||||
'states': ['initial', 'running', 'confirming', 'installing', 'completed', 'error'],
|
||||
'transitions': {
|
||||
'initial': ['running'],
|
||||
'running': ['confirming', 'installing', 'completed', 'error'],
|
||||
'confirming': ['installing', 'cancelled'],
|
||||
'installing': ['completed', 'error'],
|
||||
'completed': [],
|
||||
'error': [],
|
||||
'cancelled': []
|
||||
}
|
||||
},
|
||||
'ssh': {
|
||||
'states': ['initial', 'connecting', 'authenticating', 'connected', 'error'],
|
||||
'transitions': {
|
||||
'initial': ['connecting'],
|
||||
'connecting': ['authenticating', 'connected', 'error'],
|
||||
'authenticating': ['connected', 'error'],
|
||||
'connected': ['error'],
|
||||
'error': []
|
||||
}
|
||||
},
|
||||
'vim': {
|
||||
'states': ['initial', 'normal', 'insert', 'visual', 'command', 'exiting'],
|
||||
'transitions': {
|
||||
'initial': ['normal', 'insert'],
|
||||
'normal': ['insert', 'visual', 'command', 'exiting'],
|
||||
'insert': ['normal'],
|
||||
'visual': ['normal'],
|
||||
'command': ['normal', 'exiting'],
|
||||
'exiting': []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def detect_prompt(self, output, process_type='generic'):
|
||||
"""Detect what type of prompt is present in the output."""
|
||||
detections = {}
|
||||
|
||||
# Check all pattern categories
|
||||
for category, patterns in self.prompt_patterns.items():
|
||||
for pattern in patterns:
|
||||
if pattern.search(output):
|
||||
if category not in detections:
|
||||
detections[category] = []
|
||||
detections[category].append(pattern.pattern)
|
||||
|
||||
# Process type specific detection
|
||||
if process_type in self.prompt_patterns:
|
||||
for pattern in self.prompt_patterns[process_type]:
|
||||
if pattern.search(output):
|
||||
detections[process_type] = detections.get(process_type, [])
|
||||
detections[process_type].append(pattern.pattern)
|
||||
|
||||
return detections
|
||||
|
||||
def get_response_suggestions(self, prompt_detections, process_type='generic'):
|
||||
"""Get suggested responses based on detected prompts."""
|
||||
suggestions = []
|
||||
|
||||
for category, patterns in prompt_detections.items():
|
||||
if category == 'confirmation':
|
||||
suggestions.extend(['y', 'yes', 'n', 'no'])
|
||||
elif category == 'password':
|
||||
suggestions.append('<password>')
|
||||
elif category == 'sudo_password':
|
||||
suggestions.append('<sudo_password>')
|
||||
elif category == 'apt':
|
||||
if any('continue' in p for p in patterns):
|
||||
suggestions.extend(['y', 'yes'])
|
||||
elif category == 'vim':
|
||||
if any(':' in p for p in patterns):
|
||||
suggestions.extend(['w', 'q', 'wq', 'q!'])
|
||||
elif any('ENTER' in p for p in patterns):
|
||||
suggestions.append('\n')
|
||||
elif category == 'ssh':
|
||||
if any('yes/no' in p for p in patterns):
|
||||
suggestions.extend(['yes', 'no'])
|
||||
elif any('password' in p for p in patterns):
|
||||
suggestions.append('<password>')
|
||||
elif category == 'bash_prompt':
|
||||
suggestions.extend(['help', 'ls', 'pwd', 'exit'])
|
||||
|
||||
return list(set(suggestions)) # Remove duplicates
|
||||
|
||||
def update_session_state(self, session_name, output, process_type='generic'):
|
||||
"""Update the state machine for a session based on output."""
|
||||
if session_name not in self.session_states:
|
||||
self.session_states[session_name] = {
|
||||
'current_state': 'initial',
|
||||
'process_type': process_type,
|
||||
'last_activity': time.time(),
|
||||
'transitions': []
|
||||
}
|
||||
|
||||
session_state = self.session_states[session_name]
|
||||
old_state = session_state['current_state']
|
||||
|
||||
# Detect prompts and determine new state
|
||||
detections = self.detect_prompt(output, process_type)
|
||||
new_state = self._determine_state_from_detections(detections, process_type, old_state)
|
||||
|
||||
if new_state != old_state:
|
||||
session_state['transitions'].append({
|
||||
'from': old_state,
|
||||
'to': new_state,
|
||||
'timestamp': time.time(),
|
||||
'trigger': detections
|
||||
})
|
||||
session_state['current_state'] = new_state
|
||||
|
||||
session_state['last_activity'] = time.time()
|
||||
return new_state
|
||||
|
||||
def _determine_state_from_detections(self, detections, process_type, current_state):
|
||||
"""Determine new state based on prompt detections."""
|
||||
if process_type in self.state_machines:
|
||||
state_machine = self.state_machines[process_type]
|
||||
|
||||
# State transition logic based on detections
|
||||
if 'confirmation' in detections and current_state in ['running', 'initial']:
|
||||
return 'confirming'
|
||||
elif 'password' in detections or 'sudo_password' in detections:
|
||||
return 'authenticating'
|
||||
elif 'error' in detections:
|
||||
return 'error'
|
||||
elif 'bash_prompt' in detections and current_state != 'initial':
|
||||
return 'connected' if process_type == 'ssh' else 'completed'
|
||||
elif 'vim' in detections:
|
||||
if any('-- INSERT --' in p for p in detections.get('vim', [])):
|
||||
return 'insert'
|
||||
elif any('-- VISUAL --' in p for p in detections.get('vim', [])):
|
||||
return 'visual'
|
||||
elif any(':' in p for p in detections.get('vim', [])):
|
||||
return 'command'
|
||||
|
||||
# Default state progression
|
||||
if current_state == 'initial':
|
||||
return 'running'
|
||||
elif current_state == 'running' and detections:
|
||||
return 'waiting_input'
|
||||
elif current_state == 'waiting_input' and not detections:
|
||||
return 'running'
|
||||
|
||||
return current_state
|
||||
|
||||
def is_waiting_for_input(self, session_name):
|
||||
"""Check if a session is currently waiting for input."""
|
||||
if session_name not in self.session_states:
|
||||
return False
|
||||
|
||||
state = self.session_states[session_name]['current_state']
|
||||
process_type = self.session_states[session_name]['process_type']
|
||||
|
||||
# States that typically indicate waiting for input
|
||||
waiting_states = {
|
||||
'generic': ['waiting_input'],
|
||||
'apt': ['confirming'],
|
||||
'ssh': ['authenticating'],
|
||||
'vim': ['command', 'insert', 'visual']
|
||||
}
|
||||
|
||||
return state in waiting_states.get(process_type, [])
|
||||
|
||||
def get_session_timeout(self, session_name):
|
||||
"""Get the timeout for a session based on its process type."""
|
||||
if session_name not in self.session_states:
|
||||
return self.timeout_configs['default']
|
||||
|
||||
process_type = self.session_states[session_name]['process_type']
|
||||
return self.timeout_configs.get(process_type, self.timeout_configs['default'])
|
||||
|
||||
def check_for_timeouts(self):
|
||||
"""Check all sessions for timeouts and return timed out sessions."""
|
||||
timed_out = []
|
||||
current_time = time.time()
|
||||
|
||||
for session_name, state in self.session_states.items():
|
||||
timeout = self.get_session_timeout(session_name)
|
||||
if current_time - state['last_activity'] > timeout:
|
||||
timed_out.append(session_name)
|
||||
|
||||
return timed_out
|
||||
|
||||
def get_session_info(self, session_name):
|
||||
"""Get information about a session's state."""
|
||||
if session_name not in self.session_states:
|
||||
return None
|
||||
|
||||
state = self.session_states[session_name]
|
||||
return {
|
||||
'current_state': state['current_state'],
|
||||
'process_type': state['process_type'],
|
||||
'last_activity': state['last_activity'],
|
||||
'transitions': state['transitions'][-5:], # Last 5 transitions
|
||||
'is_waiting': self.is_waiting_for_input(session_name)
|
||||
}
|
||||
|
||||
# Global detector instance
|
||||
_detector = None
|
||||
|
||||
def get_global_detector():
|
||||
"""Get the global prompt detector instance."""
|
||||
global _detector
|
||||
if _detector is None:
|
||||
_detector = PromptDetector()
|
||||
return _detector
|
||||
Reference in New Issue
Block a user