Merge branch 'stream_v2' into '0.4.0'

Stream v2

See merge request aliasrobotics/alias_research/cai!164
This commit is contained in:
Luis Javier Navarrete Lozano 2025-05-11 13:22:10 +00:00
commit 21bd5545f4
10 changed files with 2135 additions and 774 deletions

View File

@ -8,7 +8,6 @@ license = {text = "Dual-licensed MIT and Proprietary"}
authors = [{ name = "Alias Robotics", email = "research@aliasrobotics.com" }]
dependencies = [
# logging and visualization
"flask>=2.0, <3",
"folium>=0.15.0, <1",
"matplotlib>=3.0, <4",
"numpy>=1.21, <3",
@ -26,7 +25,7 @@ dependencies = [
"prompt_toolkit>=3.0.39",
"dotenv>=0.9.9",
"litellm>=1.63.7",
"mako>=1.3.9",
"mako>=1.3.8",
"mcp; python_version >= '3.10'",
"mkdocs>=1.6.0",
"mkdocs-material>=9.6.0",

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)
@ -442,6 +442,13 @@ def run_cai_cli(starting_agent, context_variables=None, stream=False, max_turns=
}
)
# Fix message list structure BEFORE sending to the model to prevent errors
try:
from cai.util import fix_message_list
history_context = fix_message_list(history_context)
except Exception as e:
console.print(f"[yellow]Warning: Could not preprocess message history: {e}[/yellow]")
# Append the current user input as the last message in the list.
conversation_input: list | str
if history_context:
@ -483,14 +490,85 @@ def run_cai_cli(starting_agent, context_variables=None, stream=False, max_turns=
else:
# Use non-streamed response
response = asyncio.run(Runner.run(agent, conversation_input))
# Process the response items
for item in response.new_items:
# Handle tool call output items (tool results)
if isinstance(item, ToolCallOutputItem):
# First, ensure there's a corresponding assistant message with tool_calls
# before adding the tool response to prevent the OpenAI error
assistant_with_tool_call_exists = False
tool_call_id = item.raw_item["call_id"]
for msg in message_history:
if (msg.get("role") == "assistant" and
msg.get("tool_calls") and
any(tc.get("id") == tool_call_id for tc in msg.get("tool_calls", []))):
assistant_with_tool_call_exists = True
break
# If no matching assistant message exists, create one first
if not assistant_with_tool_call_exists:
tool_call_msg = {
"role": "assistant",
"content": None,
"tool_calls": [{
"id": tool_call_id,
"type": "function",
"function": {
"name": "unknown_function",
"arguments": "{}"
}
}]
}
add_to_message_history(tool_call_msg)
# Now add the tool response
tool_msg = {
"role": "tool",
"tool_call_id": item.raw_item["call_id"], # Use consistent format with streaming
"tool_call_id": tool_call_id,
"content": item.output,
}
add_to_message_history(tool_msg)
# Make sure that assistant messages with tool calls are also added to message_history
# This is especially important for non-streaming mode
if hasattr(agent, 'model'):
# Access the _Converter directly from the OpenAIChatCompletionsModel implementation
from cai.sdk.agents.models.openai_chatcompletions import _Converter
# Check if recent_tool_calls exists and process them
if hasattr(_Converter, 'recent_tool_calls'):
for call_id, call_info in _Converter.recent_tool_calls.items():
# Only process new tool calls that haven't been added to message history yet
tool_call_found = False
for msg in message_history:
if (msg.get("role") == "assistant" and
msg.get("tool_calls") and
any(tc.get("id") == call_id for tc in msg.get("tool_calls", []))):
tool_call_found = True
break
if not tool_call_found:
# Add the assistant message with the tool call
tool_call_msg = {
"role": "assistant",
"content": None,
"tool_calls": [{
"id": call_id,
"type": "function",
"function": {
"name": call_info.get('name', ''),
"arguments": call_info.get('arguments', '{}')
}
}]
}
add_to_message_history(tool_call_msg)
# Final validation to ensure message history follows OpenAI's requirements
# Ensure every tool message has a preceding assistant message with matching tool_call_id
from cai.util import fix_message_list
message_history[:] = fix_message_list(message_history)
turn_count += 1
# Stop measuring active time and start measuring idle time again

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

@ -336,6 +336,15 @@ class OpenAIChatCompletionsModel(Model):
# Log the user message
if item.get("content"):
self.logger.log_user_message(item.get("content"))
# IMPORTANT: Ensure the message list has valid tool call/result pairs
# This needs to happen before the API call to prevent errors
try:
from cai.util import fix_message_list
converted_messages = fix_message_list(converted_messages)
except Exception as e:
logger.warning(f"Failed to fix message list: {e}")
# Get token count estimate before API call for consistent counting
estimated_input_tokens, _ = count_tokens_with_tiktoken(converted_messages)
@ -411,6 +420,10 @@ class OpenAIChatCompletionsModel(Model):
# Only display the agent message if we haven't already shown the tool output
if should_display_message:
# Ensure we're in non-streaming mode for proper markdown parsing
previous_stream_setting = os.environ.get('CAI_STREAM', 'false')
os.environ['CAI_STREAM'] = 'false' # Force non-streaming mode for markdown parsing
# Print the agent message for CLI display
cli_print_agent_messages(
agent_name=getattr(self, 'agent_name', 'Agent'),
@ -433,7 +446,11 @@ class OpenAIChatCompletionsModel(Model):
interaction_cost=None,
total_cost=None,
tool_output=None, # Don't pass tool output here, we're using direct display
suppress_empty=True # Suppress empty panels
)
# Restore previous streaming setting
os.environ['CAI_STREAM'] = previous_stream_setting
# --- Add assistant tool call to message_history if present ---
# If the response contains tool_calls, add them to message_history as assistant messages
@ -457,6 +474,22 @@ class OpenAIChatCompletionsModel(Model):
}
add_to_message_history(tool_call_msg)
# Save the tool call details for later matching with output
# This is important for non-streaming mode to track tool calls properly
if not hasattr(_Converter, 'recent_tool_calls'):
_Converter.recent_tool_calls = {}
# Store the tool call by ID for later reference
import time
_Converter.recent_tool_calls[tool_call.id] = {
'name': tool_call.function.name,
'arguments': tool_call.function.arguments,
'start_time': time.time(),
'execution_info': {
'start_time': time.time()
}
}
# Log the assistant tool call message
tool_calls_list = []
@ -583,7 +616,7 @@ class OpenAIChatCompletionsModel(Model):
stop_idle_timer()
start_active_timer()
# Check if streaming should be shown in rich panel
# --- Check if streaming should be shown in rich panel ---
should_show_rich_stream = os.getenv('CAI_STREAM', 'false').lower() == 'true' and not self.disable_rich_streaming
# Create streaming context if needed
@ -922,6 +955,55 @@ class OpenAIChatCompletionsModel(Model):
if tool_call_msg not in streamed_tool_calls:
streamed_tool_calls.append(tool_call_msg)
add_to_message_history(tool_call_msg)
# NEW: Display tool call immediately when detected in streaming mode
# But only if it has complete arguments and name
if (state.function_calls[tc_index].name and
state.function_calls[tc_index].arguments and
state.function_calls[tc_index].call_id):
# First, finish any existing streaming context if it exists
if streaming_context:
try:
finish_agent_streaming(streaming_context, None)
streaming_context = None
except Exception:
pass
# Create a message-like object for displaying the function call
tool_msg = type('ToolCallStreamDisplay', (), {
'content': None,
'tool_calls': [
type('ToolCallDetail', (), {
'function': type('FunctionDetail', (), {
'name': state.function_calls[tc_index].name,
'arguments': state.function_calls[tc_index].arguments
}),
'id': state.function_calls[tc_index].call_id,
'type': 'function'
})
]
})
# Display the tool call during streaming
cli_print_agent_messages(
agent_name=getattr(self, 'agent_name', 'Agent'),
message=tool_msg,
counter=getattr(self, 'interaction_counter', 0),
model=str(self.model),
debug=False,
interaction_input_tokens=estimated_input_tokens,
interaction_output_tokens=estimated_output_tokens,
interaction_reasoning_tokens=0, # Not available during streaming yet
total_input_tokens=getattr(self, 'total_input_tokens', 0) + estimated_input_tokens,
total_output_tokens=getattr(self, 'total_output_tokens', 0) + estimated_output_tokens,
total_reasoning_tokens=getattr(self, 'total_reasoning_tokens', 0),
interaction_cost=None,
total_cost=None,
tool_output=None, # Will be shown once tool is executed
suppress_empty=True # Prevent empty panels
)
# Set flag to suppress final output to avoid duplication
self.suppress_final_output = True
except Exception as e:
# Ensure streaming context is cleaned up in case of errors
@ -983,8 +1065,15 @@ class OpenAIChatCompletionsModel(Model):
)
# Display the tool call in CLI
from cai.util import cli_print_agent_messages
try:
# First, finish any existing streaming context if it exists
if streaming_context:
try:
finish_agent_streaming(streaming_context, None)
streaming_context = None
except Exception:
pass
# Create a message-like object to display the function call
tool_msg = type('ToolCallWrapper', (), {
'content': None,
@ -1015,8 +1104,12 @@ class OpenAIChatCompletionsModel(Model):
total_reasoning_tokens=getattr(self, 'total_reasoning_tokens', 0),
interaction_cost=None,
total_cost=None,
tool_output=None # Will be shown once the tool is executed
tool_output=None, # Will be shown once the tool is executed
suppress_empty=True # Suppress empty panels during streaming
)
# Set flag to suppress final output to avoid duplication
self.suppress_final_output = True
except Exception as e:
logger.error(f"Error displaying tool call in CLI: {e}")
@ -1220,6 +1313,7 @@ class OpenAIChatCompletionsModel(Model):
direct_stats["total_cost"] = float(total_cost)
# Use the direct copy with guaranteed float costs
finish_agent_streaming(streaming_context, direct_stats)
streaming_context = None
# Removed extra newline after streaming completes to avoid blank lines
pass
@ -1243,8 +1337,10 @@ class OpenAIChatCompletionsModel(Model):
for tool_call in tool_call_msg.get("tool_calls", []):
tool_calls_list.append(tool_call)
self.logger.log_assistant_message(None, tool_calls_list)
# If there was only text output, add that as an assistant message
if (not streamed_tool_calls) and state.text_content_index_and_output and state.text_content_index_and_output[1].text:
# If we've already shown the tool calls directly during streaming,
# don't log the text message to avoid duplication
elif (not self.suppress_final_output) and state.text_content_index_and_output and state.text_content_index_and_output[1].text:
asst_msg = {
"role": "assistant",
"content": state.text_content_index_and_output[1].text
@ -1253,6 +1349,9 @@ class OpenAIChatCompletionsModel(Model):
# Log the assistant message
self.logger.log_assistant_message(state.text_content_index_and_output[1].text)
# Reset the suppress flag for future requests
self.suppress_final_output = False
# Log the complete response
self.logger.rec_training_data(
{
@ -1341,10 +1440,17 @@ class OpenAIChatCompletionsModel(Model):
if tracing.include_data():
span.span_data.input = converted_messages
# Ensure message list has correct structure regardless of error condition
# IMPORTANT: Always sanitize the message list to prevent tool call errors
# This is critical to fix common errors with tool/assistant sequences
try:
from cai.util import fix_message_list
prev_length = len(converted_messages)
converted_messages = fix_message_list(converted_messages)
new_length = len(converted_messages)
# Log if the message list was changed significantly
if new_length != prev_length:
logger.debug(f"Message list was fixed: {prev_length} -> {new_length} messages")
except Exception as e:
logger.warning(f"Failed to fix message list: {e}")
@ -1531,15 +1637,49 @@ class OpenAIChatCompletionsModel(Model):
elif ("An assistant message with 'tool_calls'" in str(e) or
"`tool_use` blocks must be followed by a user message with `tool_result`" in str(e) or # noqa: E501 # pylint: disable=C0301
"An assistant message with 'tool_calls' must be followed by tool messages" in str(e)): # Añadir esta condición
"An assistant message with 'tool_calls' must be followed by tool messages" in str(e) or
"messages with role 'tool' must be a response to a preceeding message with 'tool_calls'" in str(e)):
print(f"Error: {str(e)}")
# Use the pretty message history printer instead of the simple loop
try:
from cai.util import print_message_history
print("\nCurrent message sequence causing the error:")
print_message_history(kwargs["messages"], title="Message Sequence Error")
except ImportError:
# Fall back to simple printing if the function isn't available
print("\nCurrent message sequence causing the error:")
for i, msg in enumerate(kwargs["messages"]):
role = msg.get("role", "unknown")
content_type = (
"text" if isinstance(msg.get("content"), str) else
"list" if isinstance(msg.get("content"), list) else
"None" if msg.get("content") is None else
type(msg.get("content")).__name__
)
tool_calls = "with tool_calls" if msg.get("tool_calls") else ""
tool_call_id = f", tool_call_id: {msg.get('tool_call_id')}" if msg.get("tool_call_id") else ""
print(f" [{i}] {role}{tool_call_id} (content: {content_type}) {tool_calls}")
# NOTE: EDGE CASE: Report Agent CTRL C error
#
# This fix CTRL-C error when message list is incomplete
# When a tool is not finished but the LLM generates a tool call
try:
from cai.util import fix_message_list
kwargs["messages"] = fix_message_list(kwargs["messages"])
print("Attempting to fix message sequence...")
fixed_messages = fix_message_list(kwargs["messages"])
# Show the fixed messages if they're different
if fixed_messages != kwargs["messages"]:
try:
from cai.util import print_message_history
print_message_history(fixed_messages, title="Fixed Message Sequence")
except ImportError:
print("Messages fixed successfully.")
kwargs["messages"] = fixed_messages
except Exception as fix_error:
print(f"Failed to fix message sequence: {fix_error}")
@ -2042,7 +2182,11 @@ class _Converter:
if current_assistant_msg is not None:
# The API doesn't support empty arrays for tool_calls
if not current_assistant_msg.get("tool_calls"):
del current_assistant_msg["tool_calls"]
# Ensure content is not None if tool_calls are absent and content is also None
# Some models like Anthropic require some content, even if it's just a placeholder.
if current_assistant_msg.get("content") is None:
current_assistant_msg["content"] = "(No text content in this assistant message)" # Or just an empty string if preferred
current_assistant_msg.pop("tool_calls", None) # Use pop with default to avoid KeyError
result.append(current_assistant_msg)
current_assistant_msg = None
@ -2079,16 +2223,28 @@ class _Converter:
flush_assistant_message()
tool_calls_param: list[ChatCompletionMessageToolCallParam] = []
for tc in item["tool_calls"]:
function_details = tc.get("function", {})
arguments = function_details.get("arguments")
# Ensure arguments is a valid JSON string, defaulting to "{}" if empty or None
if arguments is None or (isinstance(arguments, str) and arguments.strip() == ""):
arguments = "{}"
elif isinstance(arguments, dict):
# Ensure it's a string if it's a dict (should already be string per schema)
arguments = json.dumps(arguments)
tool_calls_param.append(
ChatCompletionMessageToolCallParam(
id=tc.get("id", ""),
type=tc.get("type", "function"),
function=tc.get("function", {}),
function={
"name": function_details.get("name", "unknown_function"),
"arguments": arguments, # Use sanitized arguments
},
)
)
msg_asst: ChatCompletionAssistantMessageParam = {
"role": "assistant",
"content": item.get("content"),
"content": item.get("content"), # Content can be None here
"tool_calls": tool_calls_param,
}
result.append(msg_asst)
@ -2225,12 +2381,19 @@ class _Converter:
}
}
arguments = func_call.get("arguments") # func_call is a dict here
# Ensure arguments is a valid JSON string, defaulting to "{}" if empty or None
if arguments is None or (isinstance(arguments, str) and arguments.strip() == ""):
arguments = "{}"
elif isinstance(arguments, dict):
arguments = json.dumps(arguments)
new_tool_call = ChatCompletionMessageToolCallParam(
id=func_call["call_id"],
type="function",
function={
"name": func_call["name"],
"arguments": func_call["arguments"],
"arguments": arguments, # Use sanitized arguments
},
)
tool_calls.append(new_tool_call)
@ -2244,22 +2407,22 @@ class _Converter:
# Update execution timing if we have the start time
if hasattr(cls, 'recent_tool_calls') and call_id in cls.recent_tool_calls:
tool_call = cls.recent_tool_calls[call_id]
if 'start_time' in tool_call:
tool_call_details = cls.recent_tool_calls[call_id] # Renamed for clarity
if 'start_time' in tool_call_details:
end_time = time.time()
tool_execution_time = end_time - tool_call['start_time']
tool_execution_time = end_time - tool_call_details['start_time']
# Update the execution info
if 'execution_info' in tool_call:
tool_call['execution_info']['end_time'] = end_time
tool_call['execution_info']['tool_time'] = tool_execution_time
if 'execution_info' in tool_call_details:
tool_call_details['execution_info']['end_time'] = end_time
tool_call_details['execution_info']['tool_time'] = tool_execution_time
# If this is the first tool being executed, record the total time from conversation start
if not hasattr(cls, 'conversation_start_time'):
cls.conversation_start_time = tool_call['start_time']
cls.conversation_start_time = tool_call_details['start_time']
total_time = end_time - getattr(cls, 'conversation_start_time', tool_call['start_time'])
tool_call['execution_info']['total_time'] = total_time
total_time = end_time - getattr(cls, 'conversation_start_time', tool_call_details['start_time'])
tool_call_details['execution_info']['total_time'] = total_time
# Store the output so it can be accessed later
if not hasattr(cls, 'tool_outputs'):
@ -2270,67 +2433,73 @@ class _Converter:
# Display the tool output immediately with the matched tool call
from cai.util import cli_print_tool_output
# Check if we're in streaming mode - don't show tool output panel in streaming mode
# Look up the original tool call to get the name and arguments
tool_name = "Unknown Tool"
tool_args = {}
execution_info = {}
if hasattr(cls, 'recent_tool_calls') and call_id in cls.recent_tool_calls:
tool_call_details = cls.recent_tool_calls[call_id] # Renamed for clarity
tool_name = tool_call_details.get('name', 'Unknown Tool')
tool_args = tool_call_details.get('arguments', {})
execution_info = tool_call_details.get('execution_info', {})
# Get token counts from the OpenAIChatCompletionsModel if available
model_instance = None
for frame in inspect.stack():
if 'self' in frame.frame.f_locals:
self_obj = frame.frame.f_locals['self']
if isinstance(self_obj, OpenAIChatCompletionsModel):
model_instance = self_obj
break
# Always create a token_info dictionary, even if some values are zero
token_info = {
'interaction_input_tokens': getattr(model_instance, 'interaction_input_tokens', 0),
'interaction_output_tokens': getattr(model_instance, 'interaction_output_tokens', 0),
'interaction_reasoning_tokens': getattr(model_instance, 'interaction_reasoning_tokens', 0),
'total_input_tokens': getattr(model_instance, 'total_input_tokens', 0),
'total_output_tokens': getattr(model_instance, 'total_output_tokens', 0),
'total_reasoning_tokens': getattr(model_instance, 'total_reasoning_tokens', 0),
'model': str(getattr(model_instance, 'model', '')),
}
# Calculate costs using standard cost model
if model_instance and hasattr(model_instance, 'model'):
from cai.util import calculate_model_cost
model_name_str = str(model_instance.model) # Ensure model name is string
token_info['interaction_cost'] = calculate_model_cost(
model_name_str,
token_info['interaction_input_tokens'],
token_info['interaction_output_tokens']
)
token_info['total_cost'] = calculate_model_cost(
model_name_str,
token_info['total_input_tokens'],
token_info['total_output_tokens']
)
# Check if we're in streaming mode
is_streaming_enabled = os.environ.get('CAI_STREAM', 'false').lower() == 'true'
if is_streaming_enabled:
# Don't display tool output in streaming mode - it will be handled elsewhere
pass # Just skip the display, but preserve the tool output
else:
# For non-streaming mode, maintain the original behavior
# Look up the original tool call to get the name and arguments
if hasattr(cls, 'recent_tool_calls') and call_id in cls.recent_tool_calls:
tool_call = cls.recent_tool_calls[call_id]
tool_name = tool_call.get('name', 'Unknown Tool')
tool_args = tool_call.get('arguments', {})
execution_info = tool_call.get('execution_info', {})
# Get token counts from the OpenAIChatCompletionsModel if available
model_instance = None
for frame in inspect.stack():
if 'self' in frame.frame.f_locals:
self_obj = frame.frame.f_locals['self']
if isinstance(self_obj, OpenAIChatCompletionsModel):
model_instance = self_obj
break
# Always create a token_info dictionary, even if some values are zero
token_info = {
'interaction_input_tokens': getattr(model_instance, 'interaction_input_tokens', 0),
'interaction_output_tokens': getattr(model_instance, 'interaction_output_tokens', 0),
'interaction_reasoning_tokens': getattr(model_instance, 'interaction_reasoning_tokens', 0),
'total_input_tokens': getattr(model_instance, 'total_input_tokens', 0),
'total_output_tokens': getattr(model_instance, 'total_output_tokens', 0),
'total_reasoning_tokens': getattr(model_instance, 'total_reasoning_tokens', 0),
'model': str(getattr(model_instance, 'model', '')),
}
# Calculate costs using standard cost model
if model_instance and hasattr(model_instance, 'model'):
from cai.util import calculate_model_cost
model_name = str(model_instance.model)
token_info['interaction_cost'] = calculate_model_cost(
model_name,
token_info['interaction_input_tokens'],
token_info['interaction_output_tokens']
)
token_info['total_cost'] = calculate_model_cost(
model_name,
token_info['total_input_tokens'],
token_info['total_output_tokens']
)
# Use the cli_print_tool_output function with actual token values
cli_print_tool_output(
tool_name=tool_name,
args=tool_args,
output=output_content,
call_id=call_id, # Keep call_id for non-streaming mode
execution_info=execution_info,
token_info=token_info
)
# Always display tool output regardless of streaming mode
cli_print_tool_output(
tool_name=tool_name,
args=tool_args,
output=output_content,
call_id=call_id,
execution_info=execution_info,
token_info=token_info
)
# Continue with normal processing
flush_assistant_message()
# REMOVED THE BLOCK THAT CREATED A SYNTHETIC ASSISTANT MESSAGE HERE
# The responsibility for ensuring a preceding assistant message
# is now fully deferred to fix_message_list, called later.
# Now add the tool message
msg: ChatCompletionToolMessageParam = {
"role": "tool",
"tool_call_id": func_output["call_id"],

File diff suppressed because it is too large Load Diff

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

File diff suppressed because it is too large Load Diff