Streamv2beta

This commit is contained in:
luijait 2025-05-11 10:42:51 +02:00
parent c16d901498
commit 82eb56409a
9 changed files with 266 additions and 69 deletions

1
hola_mundo.py Normal file
View File

@ -0,0 +1 @@
print('Hola mundo')

View File

@ -355,7 +355,7 @@ def run_cai_cli(starting_agent, context_variables=None, stream=False, max_turns=
title="[bold]Session Summary[/bold]",
title_align="left"
)
console.print(time_panel)
console.print(time_panel, end="")
print_session_summary(console, metrics, logging_path)

View File

@ -238,7 +238,7 @@ class AgentCommand(Command):
os.environ["CAI_AGENT_TYPE"] = selected_agent_key
console.print(
f"[green]Switched to agent: {agent_name}[/green]")
f"[green]Switched to agent: {agent_name}[/green]", end="")
visualize_agent_graph(agent)
return True

View File

@ -421,7 +421,7 @@ class ModelCommand(Command):
change_message,
border_style="green",
title="Model Changed"
)
), end=""
)
return True

View File

@ -177,7 +177,7 @@ def display_banner(console: Console):
[white] Bug bounty-ready AI[/white]
"""
console.print(banner)
console.print(banner, end="")
# # Create a table showcasing CAI framework capabilities
# #
@ -361,4 +361,4 @@ def display_quick_guide(console: Console):
border_style="blue",
padding=(1, 2),
title_align="center"
))
), end="")

View File

@ -96,7 +96,7 @@ def get_user_input(
# Get user input with all features
return prompt(
[('class:prompt', '\nCAI> ')],
[('class:prompt', 'CAI> ')],
completer=command_completer,
style=create_prompt_style(),
history=FileHistory(str(history_file)),

View File

@ -434,7 +434,7 @@ def _run_ssh(command, stdout=False, timeout=100, workspace_dir=None):
return error_msg
def _run_local(command, stdout=False, timeout=100, stream=False, call_id=None, tool_name=None, workspace_dir=None):
def _run_local(command, stdout=False, timeout=100, stream=False, call_id=None, tool_name=None, workspace_dir=None, custom_args=None):
"""Runs command locally in the specified workspace_dir."""
# Make sure we're in active time mode for tool execution
stop_idle_timer()
@ -453,17 +453,17 @@ def _run_local(command, stdout=False, timeout=100, stream=False, call_id=None, t
# Parse command into parts for display
parts = command.strip().split(' ', 1)
cmd = parts[0] if parts else ""
cmd_var = parts[0] if parts else ""
args_param_val = parts[1] if len(parts) > 1 else "" # Renamed to avoid conflict with tool_args dict key
# For generic Linux commands, standardize the tool_name format
if not tool_name:
tool_name = f"{cmd}_command" if cmd else "command"
tool_name = f"{cmd_var}_command" if cmd_var else "command"
# Create args dictionary with non-empty values only
tool_args = {}
if cmd:
tool_args["command"] = cmd
if cmd_var:
tool_args["command"] = cmd_var
if args_param_val and args_param_val.strip():
tool_args["args"] = args_param_val
@ -471,9 +471,16 @@ def _run_local(command, stdout=False, timeout=100, stream=False, call_id=None, t
tool_args["workspace"] = os.path.basename(target_dir)
tool_args["full_command"] = command
# If custom args were provided, merge them with the default args
if custom_args is not None:
if isinstance(custom_args, dict):
# Merge the dictionaries, with custom args taking precedence
for key, value in custom_args.items():
tool_args[key] = value
# For generic commands, ensure we have a unique call_id
if not call_id:
call_id = f"cmd_{cmd}_{str(uuid.uuid4())[:8]}"
call_id = f"cmd_{cmd_var}_{str(uuid.uuid4())[:8]}"
# Initialize/use the call_id for this streaming session
call_id = start_tool_streaming(tool_name, tool_args, call_id)
@ -561,11 +568,11 @@ def _run_local(command, stdout=False, timeout=100, stream=False, call_id=None, t
# If this is the same command that was recently streamed, don't display it again
# We'll create a similar tool_args structure to what streaming would use
parts = command.strip().split(' ', 1)
cmd = parts[0] if parts else ""
cmd_var = parts[0] if parts else ""
args_param_val = parts[1] if len(parts) > 1 else ""
# Generate a standard tool name for consistency with streaming
standard_tool_name = tool_name or (f"{cmd}_command" if cmd else "command")
standard_tool_name = tool_name or (f"{cmd_var}_command" if cmd_var else "command")
# Calculate a consistent command key - must match the format used in cli_print_tool_output
command_key = f"{standard_tool_name}:{args_param_val}"
@ -575,7 +582,37 @@ def _run_local(command, stdout=False, timeout=100, stream=False, call_id=None, t
return output.strip()
if stdout:
print(f"\033[32m{context_msg} $ {original_cmd_for_msg}\n{output}\033[0m")
# Create a tool_args dictionary for non-streaming display
# that matches the format used in streaming
tool_display_args = {
"command": cmd_var,
"args": args_param_val,
"full_command": command,
"workspace": os.path.basename(target_dir)
}
# If custom args were provided, merge them with the default args
if custom_args is not None and isinstance(custom_args, dict):
for key, value in custom_args.items():
tool_display_args[key] = value
# Calculate execution info
tool_execution_time = time.time() - process_start_time
exec_info = {
"status": "completed" if result.returncode == 0 else "error",
"return_code": result.returncode,
"environment": "Local",
"host": os.path.basename(target_dir),
"tool_time": tool_execution_time
}
# Display the command output with rich formatting
cli_print_tool_output(
tool_name=standard_tool_name,
args=tool_display_args,
output=output,
execution_info=exec_info
)
return output.strip()
except subprocess.TimeoutExpired as e:
@ -587,13 +624,13 @@ def _run_local(command, stdout=False, timeout=100, stream=False, call_id=None, t
from cai.util import finish_tool_streaming
# Parse the command the same way we did for streaming
parts = command.strip().split(' ', 1)
cmd = parts[0] if parts else ""
args = parts[1] if len(parts) > 1 else ""
cmd_var = parts[0] if parts else ""
args_var = parts[1] if len(parts) > 1 else ""
# Ensure tool_args has complete information
tool_args = {
"command": cmd,
"args": args if args.strip() else "",
"command": cmd_var,
"args": args_var if args_var.strip() else "",
"full_command": command,
"environment": "Local",
"workspace": os.path.basename(target_dir)
@ -604,7 +641,7 @@ def _run_local(command, stdout=False, timeout=100, stream=False, call_id=None, t
"environment": "Local",
"host": os.path.basename(target_dir)
}
finish_tool_streaming(tool_name or f"{cmd}_command", tool_args, error_msg, call_id, execution_info)
finish_tool_streaming(tool_name or f"{cmd_var}_command", tool_args, error_msg, call_id, execution_info)
if stdout:
print("\033[32m" + error_msg + "\033[0m")
@ -618,13 +655,13 @@ def _run_local(command, stdout=False, timeout=100, stream=False, call_id=None, t
from cai.util import finish_tool_streaming
# Parse the command the same way we did for streaming
parts = command.strip().split(' ', 1)
cmd = parts[0] if parts else ""
args = parts[1] if len(parts) > 1 else ""
cmd_var = parts[0] if parts else ""
args_var = parts[1] if len(parts) > 1 else ""
# Ensure tool_args has complete information
tool_args = {
"command": cmd,
"args": args if args.strip() else "",
"command": cmd_var,
"args": args_var if args_var.strip() else "",
"full_command": command,
"environment": "Local",
"workspace": os.path.basename(target_dir)
@ -635,7 +672,7 @@ def _run_local(command, stdout=False, timeout=100, stream=False, call_id=None, t
"environment": "Local",
"host": os.path.basename(target_dir)
}
finish_tool_streaming(tool_name or f"{cmd}_command", tool_args, error_msg, call_id, execution_info)
finish_tool_streaming(tool_name or f"{cmd_var}_command", tool_args, error_msg, call_id, execution_info)
print(color(error_msg, fg="red"))
return error_msg
@ -647,7 +684,7 @@ def _run_local(command, stdout=False, timeout=100, stream=False, call_id=None, t
def run_command(command, ctf=None, stdout=False, # pylint: disable=too-many-arguments # noqa: E501
async_mode=False, session_id=None,
timeout=100, stream=False, call_id=None, tool_name=None):
timeout=100, stream=False, call_id=None, tool_name=None, args=None):
"""
Run command in the appropriate environment (Docker, CTF, SSH, Local)
and workspace.
@ -663,6 +700,7 @@ def run_command(command, ctf=None, stdout=False, # pylint: disable=too-many-arg
call_id: Unique ID for the command execution (for streaming)
tool_name: Name of the tool being executed (for display in streaming output).
If None, the tool name will be derived from the command.
args: Additional arguments for the tool (for display and context).
Returns:
str: Command output, status message, or session ID.
@ -673,17 +711,17 @@ def run_command(command, ctf=None, stdout=False, # pylint: disable=too-many-arg
# Parse command into standard parts to ensure consistent naming
parts = command.strip().split(' ', 1)
cmd = parts[0] if parts else ""
args = parts[1] if len(parts) > 1 else ""
cmd_name = parts[0] if parts else ""
cmd_args = parts[1] if len(parts) > 1 else ""
# Generate a call_id if we're streaming and one wasn't provided
# Use a more specific format that includes the command name for easier tracking
if not call_id and stream:
call_id = f"cmd_{cmd}_{str(uuid.uuid4())[:8]}"
call_id = f"cmd_{cmd_name}_{str(uuid.uuid4())[:8]}"
# If no tool_name is provided, derive it from the command in a consistent way
if not tool_name:
tool_name = f"{cmd}_command" if cmd else "command"
tool_name = f"{cmd_name}_command" if cmd_name else "command"
try:
# If session_id is provided, send command to that session
@ -750,8 +788,8 @@ def run_command(command, ctf=None, stdout=False, # pylint: disable=too-many-arg
# Create args dictionary with standardized format
tool_args = {
"command": cmd,
"args": args if args.strip() else "",
"command": cmd_name,
"args": cmd_args if cmd_args.strip() else "",
"full_command": command,
"container": container_id[:12],
"environment": "Container",
@ -887,7 +925,7 @@ def run_command(command, ctf=None, stdout=False, # pylint: disable=too-many-arg
start_idle_timer()
# Fallback to local execution on timeout
print(color("Container execution timed out. Attempting execution on host instead.", fg="yellow"))
return _run_local(command, stdout, timeout, False, None, tool_name, _get_workspace_dir())
return _run_local(command, stdout, timeout, False, None, tool_name, _get_workspace_dir(), args)
except Exception as e:
# Handle other errors
@ -908,7 +946,7 @@ def run_command(command, ctf=None, stdout=False, # pylint: disable=too-many-arg
start_idle_timer()
# Fallback to local execution on error
print(color("Container execution failed. Attempting execution on host instead.", fg="yellow"))
return _run_local(command, stdout, timeout, False, None, tool_name, _get_workspace_dir())
return _run_local(command, stdout, timeout, False, None, tool_name, _get_workspace_dir(), args)
# Handle Synchronous Execution in Container
try:
@ -945,7 +983,7 @@ def run_command(command, ctf=None, stdout=False, # pylint: disable=too-many-arg
stop_active_timer()
start_idle_timer()
# Fallback to local execution, preserving workspace context
return _run_local(command, stdout, timeout, stream, call_id, tool_name, _get_workspace_dir()) # noqa E501
return _run_local(command, stdout, timeout, stream, call_id, tool_name, _get_workspace_dir(), args) # noqa E501
# Switch back to idle mode after command completes
stop_active_timer()
@ -961,7 +999,7 @@ def run_command(command, ctf=None, stdout=False, # pylint: disable=too-many-arg
stop_active_timer()
start_idle_timer()
# Fallback to local execution on timeout
return _run_local(command, stdout, timeout, stream, call_id, tool_name, _get_workspace_dir()) # noqa E501
return _run_local(command, stdout, timeout, stream, call_id, tool_name, _get_workspace_dir(), args) # noqa E501
except Exception as e: # pylint: disable=broad-except
error_msg = f"Error executing command in container: {str(e)}"
print(color(f"{context_msg} {error_msg}", fg="red"))
@ -970,7 +1008,7 @@ def run_command(command, ctf=None, stdout=False, # pylint: disable=too-many-arg
stop_active_timer()
start_idle_timer()
# Fallback to local execution on other errors
return _run_local(command, stdout, timeout, stream, call_id, tool_name, _get_workspace_dir()) # noqa E501
return _run_local(command, stdout, timeout, stream, call_id, tool_name, _get_workspace_dir(), args) # noqa E501
# --- CTF Execution ---
if ctf:
@ -981,8 +1019,8 @@ def run_command(command, ctf=None, stdout=False, # pylint: disable=too-many-arg
# Create args dictionary with standardized format
tool_args = {
"command": cmd,
"args": args if args.strip() else "",
"command": cmd_name,
"args": cmd_args if cmd_args.strip() else "",
"full_command": command,
"environment": "CTF",
"workspace": os.path.basename(_get_workspace_dir())
@ -1066,8 +1104,8 @@ def run_command(command, ctf=None, stdout=False, # pylint: disable=too-many-arg
# Create args dictionary with standardized format
tool_args = {
"command": cmd,
"args": args if args.strip() else "",
"command": cmd_name,
"args": cmd_args if cmd_args.strip() else "",
"full_command": command,
"ssh_host": ssh_connection,
"environment": "SSH"
@ -1213,7 +1251,9 @@ def run_command(command, ctf=None, stdout=False, # pylint: disable=too-many-arg
timeout,
stream=stream,
call_id=call_id,
tool_name=tool_name
tool_name=tool_name,
workspace_dir=_get_workspace_dir(),
custom_args=args
)
# Switch back to idle mode after local command completes

View File

@ -37,62 +37,101 @@ def execute_code(code: str = "", language: str = "python",
"python": "py",
"php": "php",
"bash": "sh",
"shell": "sh", # Add shell as alias for bash
"ruby": "rb",
"perl": "pl",
"golang": "go",
"go": "go", # Add go as alias for golang
"javascript": "js",
"js": "js", # Add js as alias for javascript
"typescript": "ts",
"ts": "ts", # Add ts as alias for typescript
"rust": "rs",
"csharp": "cs",
"cs": "cs", # Add cs as alias for csharp
"java": "java",
"kotlin": "kt"
"kotlin": "kt",
"c": "c", # Add C language
"cpp": "cpp", # Add C++ language
"c++": "cpp" # Add C++ language alias
}
ext = extensions.get(language.lower(), "txt")
# Normalize language to lowercase
language = language.lower()
ext = extensions.get(language, "txt")
full_filename = f"{filename}.{ext}"
# Create code file with content
create_cmd = f"cat << 'EOF' > {full_filename}\n{code}\nEOF"
result = run_command(create_cmd, ctf=ctf)
result = run_command(create_cmd, ctf=ctf, stream=True, tool_name="execute_code")
if "error" in result.lower():
return f"Failed to create code file: {result}"
if language.lower() == "python":
# Prepare execution command based on language
if language in ["python", "py"]:
exec_cmd = f"python3 {full_filename}"
elif language.lower() == "php":
elif language in ["php"]:
exec_cmd = f"php {full_filename}"
elif language.lower() in ["bash", "sh"]:
elif language in ["bash", "sh", "shell"]:
exec_cmd = f"bash {full_filename}"
elif language.lower() == "ruby":
elif language in ["ruby", "rb"]:
exec_cmd = f"ruby {full_filename}"
elif language.lower() == "perl":
elif language in ["perl", "pl"]:
exec_cmd = f"perl {full_filename}"
elif language.lower() == "golang" or language.lower() == "go":
elif language in ["golang", "go"]:
temp_dir = f"/tmp/go_exec_{filename}"
run_command(f"mkdir -p {temp_dir}", ctf=ctf)
run_command(f"cp {full_filename} {temp_dir}/main.go", ctf=ctf)
run_command(f"cd {temp_dir} && go mod init temp", ctf=ctf)
exec_cmd = f"cd {temp_dir} && go run main.go"
elif language.lower() == "javascript":
elif language in ["javascript", "js"]:
exec_cmd = f"node {full_filename}"
elif language.lower() == "typescript":
elif language in ["typescript", "ts"]:
exec_cmd = f"ts-node {full_filename}"
elif language.lower() == "rust":
elif language in ["rust", "rs"]:
# For Rust, we need to compile first
run_command(f"rustc {full_filename} -o {filename}", ctf=ctf)
exec_cmd = f"./{filename}"
elif language.lower() == "csharp":
elif language in ["csharp", "cs"]:
# For C#, compile with dotnet
run_command(f"dotnet build {full_filename}", ctf=ctf)
exec_cmd = f"dotnet run {full_filename}"
elif language.lower() == "java":
elif language in ["java"]:
# For Java, compile first
run_command(f"javac {full_filename}", ctf=ctf)
exec_cmd = f"java {filename}"
elif language.lower() == "kotlin":
elif language in ["kotlin", "kt"]:
# For Kotlin, compile first
run_command(f"kotlinc {full_filename} -include-runtime -d {filename}.jar", ctf=ctf)
exec_cmd = f"java -jar {filename}.jar"
elif language in ["c"]:
# For C, compile with gcc
run_command(f"gcc {full_filename} -o {filename}", ctf=ctf)
exec_cmd = f"./{filename}"
elif language in ["cpp", "c++"]:
# For C++, compile with g++
run_command(f"g++ {full_filename} -o {filename}", ctf=ctf)
exec_cmd = f"./{filename}"
else:
return f"Unsupported language: {language}"
output = run_command(exec_cmd, ctf=ctf, timeout=timeout)
# Execute the code with syntax-highlighted output
# Create a custom tool args dictionary to send language and code info to the tool output function
tool_args = {
"command": "execute",
"language": language,
"filename": filename,
"code": code, # Include the code for syntax highlighting
"timeout": timeout
}
# Run the command with streaming to get syntax highlighting
output = run_command(
exec_cmd,
ctf=ctf,
timeout=timeout,
stream=True,
tool_name="execute_code",
args=tool_args
)
return output

View File

@ -22,6 +22,10 @@ from dataclasses import dataclass, field
from typing import Dict, Optional
import time
import threading
from rich.syntax import Syntax # Import Syntax for highlighting
from rich.panel import Panel
from rich.console import Group
from rich.box import ROUNDED
# Global timing variables for tracking active and idle time
_active_timer_start = None
@ -1183,7 +1187,7 @@ def create_agent_streaming_context(agent_name, counter, model):
Text.assemble(header, content, footer),
border_style="blue",
box=ROUNDED,
padding=(1, 2),
padding=(0, 1),
title="[bold]Agent Streaming Response[/bold]",
title_align="left",
width=panel_width,
@ -1244,7 +1248,7 @@ def update_agent_streaming_content(context, text_delta):
Text.assemble(context["header"], context["content"], context["footer"]),
border_style="blue",
box=ROUNDED,
padding=(1, 2),
padding=(0, 1),
title="[bold]Agent Streaming Response[/bold]",
title_align="left",
width=context.get("panel_width", 100),
@ -1349,13 +1353,12 @@ def finish_agent_streaming(context, final_stats=None):
Text.assemble(
context["header"],
context["content"],
Text("\n\n"),
tokens_text if tokens_text else Text(""),
tokens_text if tokens_text else Text(""),
context["footer"]
),
border_style="blue",
box=ROUNDED,
padding=(1, 2),
padding=(0, 1),
title="[bold]Agent Streaming Response[/bold]",
title_align="left",
width=context.get("panel_width", 100),
@ -1522,7 +1525,7 @@ def cli_print_tool_output(tool_name="", args="", output="", call_id=None, execut
content,
title=title,
border_style=border_style,
padding=(1, 2),
padding=(0, 1),
box=ROUNDED,
title_align="left"
)
@ -1586,18 +1589,34 @@ def cli_print_tool_output(tool_name="", args="", output="", call_id=None, execut
# Create a console for output
console = Console(theme=theme)
# Get the panel content
# Get the panel content - with syntax highlighting
header, content = _create_tool_panel_content(tool_name, args, output, execution_info, token_info)
# Determine border style
# Determine border style based on status
border_style = "blue" # Default for non-streaming
if execution_info:
status = execution_info.get('status', 'completed')
if status == "completed":
border_style = "green"
title = "[bold green]Tool Output [Completed][/bold green]"
elif status == "error":
border_style = "red"
title = "[bold red]Tool Output [Error][/bold red]"
elif status == "timeout":
border_style = "red"
title = "[bold red]Tool Output [Timeout][/bold red]"
else:
title = "[bold blue]Tool Output[/bold blue]"
else:
title = "[bold blue]Tool Output[/bold blue]"
# Create the panel
panel = Panel(
content,
title="[bold blue]Tool Output[/bold blue]",
title=title,
border_style=border_style,
padding=(1, 2),
padding=(0, 1),
box=ROUNDED,
title_align="left"
)
@ -1613,6 +1632,10 @@ def cli_print_tool_output(tool_name="", args="", output="", call_id=None, execut
def _create_tool_panel_content(tool_name, args, output, execution_info=None, token_info=None):
"""Create the header and content for a tool output panel."""
from rich.text import Text
from rich.syntax import Syntax # Import Syntax for highlighting
from rich.panel import Panel
from rich.console import Group
from rich.box import ROUNDED
# Format arguments for display
args_str = _format_tool_args(args)
@ -1655,7 +1678,101 @@ def _create_tool_panel_content(tool_name, args, output, execution_info=None, tok
# Create token information if available
token_content = _create_token_info_display(token_info)
# Assemble the full content
# Determine if we need specialized content formatting
group_content = [header]
# Special handling for execute_code tool
if tool_name == "execute_code":
try:
# Parse args to get language and code
language = "python" # Default
code = ""
filename = "exploit"
if isinstance(args, dict):
language = args.get("language", "python")
code = args.get("code", "")
filename = args.get("filename", "exploit")
elif isinstance(args, str):
# Try to extract language and code from args string
import re
lang_match = re.search(r"language\s*=\s*['\"]?([a-zA-Z0-9+]+)['\"]?", args)
if lang_match:
language = lang_match.group(1)
# Create syntax highlighted code panel if we have code
if code:
syntax = Syntax(code, language, theme="monokai", line_numbers=True,
background_color="#272822", indent_guides=True)
code_panel = Panel(
syntax,
title=f"Code ({language})",
border_style="cyan",
title_align="left",
box=ROUNDED,
padding=(0, 1)
)
# Create output panel with proper highlighting
output_syntax = Syntax(output, "text", theme="monokai",
background_color="#272822")
output_panel = Panel(
output_syntax,
title="Output",
border_style="green",
title_align="left",
box=ROUNDED,
padding=(0, 1)
)
# Add code and output panels to content
group_content.append("\n")
group_content.append(code_panel)
group_content.append("\n")
group_content.append(output_panel)
# Add token info if available
if token_content:
group_content.append("\n")
group_content.append(token_content)
return header, Group(*group_content)
except Exception:
# Fallback if syntax highlighting fails
pass
# Special handling for generic_linux_command
elif tool_name == "generic_linux_command" or "command" in tool_name:
try:
# Highlight the output as bash
output_syntax = Syntax(output, "bash", theme="monokai",
background_color="#272822")
# Create a panel for the formatted output
output_panel = Panel(
output_syntax,
title="Command Output",
border_style="green",
title_align="left",
box=ROUNDED,
padding=(0, 1)
)
# Assemble content with highlighted output
group_content.append("\n")
group_content.append(output_panel)
# Add token info if available
if token_content:
group_content.append("\n")
group_content.append(token_content)
return header, Group(*group_content)
except Exception:
# Fallback if syntax highlighting fails
pass
# Default content assembly for other tools
content = Text()
content.append(header)
content.append("\n\n")