cowork-dash 0.1.9__py3-none-any.whl → 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cowork_dash/agent.py +32 -11
- cowork_dash/app.py +591 -67
- cowork_dash/assets/app.js +34 -0
- cowork_dash/assets/styles.css +788 -697
- cowork_dash/cli.py +9 -0
- cowork_dash/components.py +398 -55
- cowork_dash/config.py +12 -1
- cowork_dash/file_utils.py +43 -4
- cowork_dash/layout.py +2 -2
- cowork_dash/sandbox.py +361 -0
- cowork_dash/tools.py +640 -38
- {cowork_dash-0.1.9.dist-info → cowork_dash-0.2.0.dist-info}/METADATA +1 -1
- cowork_dash-0.2.0.dist-info/RECORD +23 -0
- cowork_dash-0.1.9.dist-info/RECORD +0 -22
- {cowork_dash-0.1.9.dist-info → cowork_dash-0.2.0.dist-info}/WHEEL +0 -0
- {cowork_dash-0.1.9.dist-info → cowork_dash-0.2.0.dist-info}/entry_points.txt +0 -0
- {cowork_dash-0.1.9.dist-info → cowork_dash-0.2.0.dist-info}/licenses/LICENSE +0 -0
cowork_dash/tools.py
CHANGED
|
@@ -12,6 +12,33 @@ from .config import WORKSPACE_ROOT, VIRTUAL_FS
|
|
|
12
12
|
from .canvas import parse_canvas_object, generate_canvas_id
|
|
13
13
|
|
|
14
14
|
|
|
15
|
+
# =============================================================================
|
|
16
|
+
# DISPLAY INLINE - PUSH TO AGENT STATE
|
|
17
|
+
# =============================================================================
|
|
18
|
+
# Instead of returning data through LangGraph (which has serialization limitations),
|
|
19
|
+
# display_inline pushes items directly to the agent state. The Dash polling callback
|
|
20
|
+
# picks them up from state["display_inline_items"] and renders them directly.
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _push_display_inline_to_state(item: Dict[str, Any]) -> None:
|
|
24
|
+
"""Push a display_inline item to the agent state.
|
|
25
|
+
|
|
26
|
+
Uses the current session's state via push_display_inline_item from app.py.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
item: The display result dict with type, display_type, data, etc.
|
|
30
|
+
"""
|
|
31
|
+
# Import here to avoid circular import
|
|
32
|
+
from .app import push_display_inline_item
|
|
33
|
+
|
|
34
|
+
# Get session_id from tool context (set during agent execution)
|
|
35
|
+
session_id = get_tool_session_context()
|
|
36
|
+
push_display_inline_item(item, session_id)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# =============================================================================
|
|
40
|
+
|
|
41
|
+
|
|
15
42
|
# Memory limit for cell execution (in bytes)
|
|
16
43
|
# Default: 512 MB - can be overridden via environment variable
|
|
17
44
|
CELL_MEMORY_LIMIT_MB = int(os.environ.get("COWORK_CELL_MEMORY_LIMIT_MB", "512"))
|
|
@@ -785,35 +812,6 @@ def reset_notebook() -> Dict[str, Any]:
|
|
|
785
812
|
return _notebook_state.reset()
|
|
786
813
|
|
|
787
814
|
|
|
788
|
-
def get_notebook_canvas_items() -> List[Dict[str, Any]]:
|
|
789
|
-
"""
|
|
790
|
-
Get all canvas items generated during notebook cell execution.
|
|
791
|
-
|
|
792
|
-
When code in cells calls add_to_canvas(), the items are collected here.
|
|
793
|
-
Use this to retrieve visualizations generated by executed code.
|
|
794
|
-
|
|
795
|
-
Returns:
|
|
796
|
-
List of canvas item dictionaries with type and data
|
|
797
|
-
|
|
798
|
-
Examples:
|
|
799
|
-
# After executing cells that created charts
|
|
800
|
-
items = get_notebook_canvas_items()
|
|
801
|
-
for item in items:
|
|
802
|
-
print(f"Type: {item['type']}")
|
|
803
|
-
"""
|
|
804
|
-
return _notebook_state.get_canvas_items()
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
def clear_notebook_canvas_items() -> Dict[str, Any]:
|
|
808
|
-
"""
|
|
809
|
-
Clear all canvas items collected from notebook execution.
|
|
810
|
-
|
|
811
|
-
Returns:
|
|
812
|
-
Dictionary with count of cleared items
|
|
813
|
-
"""
|
|
814
|
-
return _notebook_state.clear_canvas_items()
|
|
815
|
-
|
|
816
|
-
|
|
817
815
|
# =============================================================================
|
|
818
816
|
# CANVAS TOOLS
|
|
819
817
|
# =============================================================================
|
|
@@ -941,6 +939,549 @@ def remove_canvas_item(item_id: str) -> Dict[str, Any]:
|
|
|
941
939
|
}
|
|
942
940
|
|
|
943
941
|
|
|
942
|
+
# =============================================================================
|
|
943
|
+
# DISPLAY INLINE TOOL
|
|
944
|
+
# =============================================================================
|
|
945
|
+
|
|
946
|
+
def display_inline(
|
|
947
|
+
content: Any,
|
|
948
|
+
title: Optional[str] = None,
|
|
949
|
+
display_type: Optional[str] = None
|
|
950
|
+
) -> str:
|
|
951
|
+
"""Display content to the user in a rich, interactive format inline in the chat.
|
|
952
|
+
|
|
953
|
+
This tool renders content directly in the conversation for immediate visibility.
|
|
954
|
+
Use this when you want the user to see results right away without navigating to
|
|
955
|
+
the canvas. Supports images, HTML, Plotly charts, CSV/DataFrames, and more.
|
|
956
|
+
|
|
957
|
+
**Note:** Returns a JSON string that will be parsed by the UI for rendering.
|
|
958
|
+
This is necessary because LangGraph converts non-string tool results to strings,
|
|
959
|
+
and this ensures proper JSON serialization instead of Python repr().
|
|
960
|
+
|
|
961
|
+
Args:
|
|
962
|
+
content: The content to display. Can be:
|
|
963
|
+
- str: File path (image, HTML, CSV, JSON) or raw content
|
|
964
|
+
- bytes: Binary image data (PNG, JPEG, etc.)
|
|
965
|
+
- pd.DataFrame: Rendered as an interactive table
|
|
966
|
+
- dict: If it looks like Plotly JSON, rendered as a chart
|
|
967
|
+
- matplotlib.figure.Figure: Rendered as an image
|
|
968
|
+
- plotly.graph_objects.Figure: Rendered as an interactive chart
|
|
969
|
+
title: Optional title displayed above the content
|
|
970
|
+
display_type: Optional hint for how to render the content. One of:
|
|
971
|
+
- "image": Force image rendering (PNG, JPEG, GIF, etc.)
|
|
972
|
+
- "html": Force HTML rendering with preview
|
|
973
|
+
- "plotly": Force Plotly chart rendering
|
|
974
|
+
- "csv" or "dataframe": Force table rendering
|
|
975
|
+
- "json": Force JSON rendering
|
|
976
|
+
- "text": Force plain text rendering
|
|
977
|
+
Auto-detected if not provided.
|
|
978
|
+
|
|
979
|
+
Returns:
|
|
980
|
+
Dictionary with display metadata:
|
|
981
|
+
- type: "display_inline" (identifies this as inline display)
|
|
982
|
+
- display_type: The detected/specified display type
|
|
983
|
+
- title: The title (if provided)
|
|
984
|
+
- data: The processed data for rendering
|
|
985
|
+
- status: "success" or "error"
|
|
986
|
+
|
|
987
|
+
Examples:
|
|
988
|
+
# Show an image file
|
|
989
|
+
display_inline("analysis_results.png", title="Results Chart")
|
|
990
|
+
|
|
991
|
+
# Show a DataFrame
|
|
992
|
+
import pandas as pd
|
|
993
|
+
df = pd.read_csv("data.csv")
|
|
994
|
+
display_inline(df, title="Sales Data")
|
|
995
|
+
|
|
996
|
+
# Show a Plotly figure
|
|
997
|
+
import plotly.express as px
|
|
998
|
+
fig = px.bar(df, x="category", y="value")
|
|
999
|
+
display_inline(fig, title="Category Distribution")
|
|
1000
|
+
|
|
1001
|
+
# Show HTML content
|
|
1002
|
+
display_inline("<h1>Hello</h1><p>World</p>", display_type="html")
|
|
1003
|
+
|
|
1004
|
+
# Show CSV file
|
|
1005
|
+
display_inline("report.csv", title="Monthly Report")
|
|
1006
|
+
"""
|
|
1007
|
+
# Call the implementation to get the display data
|
|
1008
|
+
result_dict = _display_inline_impl(content, title, display_type)
|
|
1009
|
+
|
|
1010
|
+
# Add unique ID for tracking
|
|
1011
|
+
import uuid
|
|
1012
|
+
result_dict["_item_id"] = str(uuid.uuid4())[:8]
|
|
1013
|
+
|
|
1014
|
+
# Push to agent state for direct Dash rendering (bypasses LangGraph serialization)
|
|
1015
|
+
_push_display_inline_to_state(result_dict)
|
|
1016
|
+
|
|
1017
|
+
# Return a simple confirmation message (this goes through LangGraph)
|
|
1018
|
+
# The actual rich content is rendered directly by Dash via state["display_inline_items"]
|
|
1019
|
+
display_type_str = result_dict.get("display_type", "content")
|
|
1020
|
+
title_str = f" '{title}'" if title else ""
|
|
1021
|
+
if result_dict.get("status") == "error":
|
|
1022
|
+
return f"Error displaying{title_str}: {result_dict.get('error', 'Unknown error')}"
|
|
1023
|
+
return f"Displayed {display_type_str}{title_str} inline."
|
|
1024
|
+
|
|
1025
|
+
|
|
1026
|
+
def _display_inline_impl(
|
|
1027
|
+
content: Any,
|
|
1028
|
+
title: Optional[str] = None,
|
|
1029
|
+
display_type: Optional[str] = None
|
|
1030
|
+
) -> Dict[str, Any]:
|
|
1031
|
+
"""Internal implementation of display_inline that returns a dict."""
|
|
1032
|
+
import base64
|
|
1033
|
+
import io
|
|
1034
|
+
import json
|
|
1035
|
+
from pathlib import PurePath
|
|
1036
|
+
|
|
1037
|
+
result = {
|
|
1038
|
+
"type": "display_inline",
|
|
1039
|
+
"display_type": None,
|
|
1040
|
+
"title": title,
|
|
1041
|
+
"data": None,
|
|
1042
|
+
"preview": None, # For thumbnails
|
|
1043
|
+
"downloadable": False,
|
|
1044
|
+
"status": "success",
|
|
1045
|
+
"error": None
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
try:
|
|
1049
|
+
# Get workspace root for file operations
|
|
1050
|
+
workspace_root = _get_workspace_root_for_context()
|
|
1051
|
+
|
|
1052
|
+
# Detect content type and process accordingly
|
|
1053
|
+
obj_type = type(content).__name__
|
|
1054
|
+
obj_module = type(content).__module__
|
|
1055
|
+
|
|
1056
|
+
# Handle file paths (strings that look like file paths)
|
|
1057
|
+
if isinstance(content, str) and not display_type:
|
|
1058
|
+
# Check if it's a file path
|
|
1059
|
+
if _is_file_path(content, workspace_root):
|
|
1060
|
+
return _process_file_for_display(content, workspace_root, title, display_type)
|
|
1061
|
+
|
|
1062
|
+
# If it looks like a file path but doesn't exist, try to find it
|
|
1063
|
+
from pathlib import PurePath
|
|
1064
|
+
ext = PurePath(content).suffix.lower()
|
|
1065
|
+
known_file_extensions = {
|
|
1066
|
+
'.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg', '.bmp', '.ico',
|
|
1067
|
+
'.html', '.htm', '.csv', '.tsv', '.json', '.pdf'
|
|
1068
|
+
}
|
|
1069
|
+
if ext in known_file_extensions and "\n" not in content and len(content) < 500:
|
|
1070
|
+
# Try to find the file - check if just filename was passed
|
|
1071
|
+
found_path = _find_file_in_workspace(content, workspace_root)
|
|
1072
|
+
if found_path:
|
|
1073
|
+
return _process_file_for_display(found_path, workspace_root, title, display_type)
|
|
1074
|
+
else:
|
|
1075
|
+
# Return error - file not found
|
|
1076
|
+
result["status"] = "error"
|
|
1077
|
+
result["display_type"] = "error"
|
|
1078
|
+
result["error"] = f"File not found: {content}"
|
|
1079
|
+
result["data"] = f"Could not find file '{content}' in workspace. Make sure the file exists and the path is correct."
|
|
1080
|
+
return result
|
|
1081
|
+
|
|
1082
|
+
# Otherwise, check for explicit display types or treat as text/HTML
|
|
1083
|
+
if content.strip().startswith("<") and ">" in content:
|
|
1084
|
+
result["display_type"] = "html"
|
|
1085
|
+
result["data"] = content
|
|
1086
|
+
result["preview"] = content[:500] + "..." if len(content) > 500 else content
|
|
1087
|
+
return result
|
|
1088
|
+
|
|
1089
|
+
# Handle explicit display_type for strings
|
|
1090
|
+
if isinstance(content, str) and display_type:
|
|
1091
|
+
if display_type == "html":
|
|
1092
|
+
result["display_type"] = "html"
|
|
1093
|
+
result["data"] = content
|
|
1094
|
+
result["preview"] = content[:500] + "..." if len(content) > 500 else content
|
|
1095
|
+
return result
|
|
1096
|
+
elif display_type == "text":
|
|
1097
|
+
result["display_type"] = "text"
|
|
1098
|
+
result["data"] = content
|
|
1099
|
+
return result
|
|
1100
|
+
elif display_type in ("csv", "dataframe"):
|
|
1101
|
+
# Parse CSV string
|
|
1102
|
+
try:
|
|
1103
|
+
import pandas as pd
|
|
1104
|
+
df = pd.read_csv(io.StringIO(content))
|
|
1105
|
+
return _process_dataframe_for_display(df, title)
|
|
1106
|
+
except Exception as e:
|
|
1107
|
+
result["display_type"] = "text"
|
|
1108
|
+
result["data"] = content
|
|
1109
|
+
result["error"] = f"Could not parse as CSV: {e}"
|
|
1110
|
+
return result
|
|
1111
|
+
elif display_type == "json":
|
|
1112
|
+
result["display_type"] = "json"
|
|
1113
|
+
try:
|
|
1114
|
+
result["data"] = json.loads(content) if isinstance(content, str) else content
|
|
1115
|
+
except json.JSONDecodeError:
|
|
1116
|
+
result["data"] = content
|
|
1117
|
+
return result
|
|
1118
|
+
elif display_type == "image":
|
|
1119
|
+
# Assume it's base64 or file path
|
|
1120
|
+
if _is_file_path(content, workspace_root):
|
|
1121
|
+
return _process_file_for_display(content, workspace_root, title, "image")
|
|
1122
|
+
result["display_type"] = "image"
|
|
1123
|
+
result["data"] = content # Assume base64
|
|
1124
|
+
return result
|
|
1125
|
+
elif display_type == "plotly":
|
|
1126
|
+
result["display_type"] = "plotly"
|
|
1127
|
+
if isinstance(content, str):
|
|
1128
|
+
result["data"] = json.loads(content)
|
|
1129
|
+
else:
|
|
1130
|
+
result["data"] = content
|
|
1131
|
+
return result
|
|
1132
|
+
|
|
1133
|
+
# Handle bytes (binary data - likely image)
|
|
1134
|
+
if isinstance(content, bytes):
|
|
1135
|
+
result["display_type"] = "image"
|
|
1136
|
+
result["data"] = base64.b64encode(content).decode('utf-8')
|
|
1137
|
+
return result
|
|
1138
|
+
|
|
1139
|
+
# Handle pandas DataFrame
|
|
1140
|
+
if obj_module.startswith('pandas') and obj_type == 'DataFrame':
|
|
1141
|
+
return _process_dataframe_for_display(content, title)
|
|
1142
|
+
|
|
1143
|
+
# Handle matplotlib Figure
|
|
1144
|
+
if obj_module.startswith('matplotlib') and 'Figure' in obj_type:
|
|
1145
|
+
buf = io.BytesIO()
|
|
1146
|
+
content.savefig(buf, format='png', bbox_inches='tight', dpi=100)
|
|
1147
|
+
buf.seek(0)
|
|
1148
|
+
img_data = buf.read()
|
|
1149
|
+
buf.close()
|
|
1150
|
+
|
|
1151
|
+
result["display_type"] = "image"
|
|
1152
|
+
result["data"] = base64.b64encode(img_data).decode('utf-8')
|
|
1153
|
+
result["mime_type"] = "image/png"
|
|
1154
|
+
return result
|
|
1155
|
+
|
|
1156
|
+
# Handle Plotly Figure
|
|
1157
|
+
if obj_module.startswith('plotly') and 'Figure' in obj_type:
|
|
1158
|
+
result["display_type"] = "plotly"
|
|
1159
|
+
result["data"] = json.loads(content.to_json())
|
|
1160
|
+
return result
|
|
1161
|
+
|
|
1162
|
+
# Handle dict (check for Plotly JSON structure or serialization artifacts)
|
|
1163
|
+
if isinstance(content, dict):
|
|
1164
|
+
# Check if this looks like a serialized matplotlib/plotly reference (common mistake)
|
|
1165
|
+
if content.get('type') in ('matplotlib', 'plotly') and 'figure' in content:
|
|
1166
|
+
result["status"] = "error"
|
|
1167
|
+
result["display_type"] = "error"
|
|
1168
|
+
result["error"] = (
|
|
1169
|
+
"Cannot display figure objects directly. The figure was serialized to a reference. "
|
|
1170
|
+
"To display matplotlib figures, save them to a file first:\n"
|
|
1171
|
+
" fig.savefig('chart.png')\n"
|
|
1172
|
+
" display_inline('chart.png')\n"
|
|
1173
|
+
"Or use add_to_canvas(fig) inside a notebook cell."
|
|
1174
|
+
)
|
|
1175
|
+
result["data"] = str(content)
|
|
1176
|
+
return result
|
|
1177
|
+
# Check for Plotly JSON structure
|
|
1178
|
+
if 'data' in content and isinstance(content.get('data'), list):
|
|
1179
|
+
result["display_type"] = "plotly"
|
|
1180
|
+
result["data"] = content
|
|
1181
|
+
return result
|
|
1182
|
+
else:
|
|
1183
|
+
result["display_type"] = "json"
|
|
1184
|
+
result["data"] = content
|
|
1185
|
+
return result
|
|
1186
|
+
|
|
1187
|
+
# Handle PIL Image
|
|
1188
|
+
if obj_module.startswith('PIL') and 'Image' in obj_type:
|
|
1189
|
+
buf = io.BytesIO()
|
|
1190
|
+
content.save(buf, format='PNG')
|
|
1191
|
+
buf.seek(0)
|
|
1192
|
+
img_data = buf.read()
|
|
1193
|
+
buf.close()
|
|
1194
|
+
|
|
1195
|
+
result["display_type"] = "image"
|
|
1196
|
+
result["data"] = base64.b64encode(img_data).decode('utf-8')
|
|
1197
|
+
result["mime_type"] = "image/png"
|
|
1198
|
+
return result
|
|
1199
|
+
|
|
1200
|
+
# Handle list (could be data for table)
|
|
1201
|
+
if isinstance(content, list) and len(content) > 0:
|
|
1202
|
+
if isinstance(content[0], dict):
|
|
1203
|
+
# List of dicts - render as table
|
|
1204
|
+
try:
|
|
1205
|
+
import pandas as pd
|
|
1206
|
+
df = pd.DataFrame(content)
|
|
1207
|
+
return _process_dataframe_for_display(df, title)
|
|
1208
|
+
except Exception:
|
|
1209
|
+
result["display_type"] = "json"
|
|
1210
|
+
result["data"] = content
|
|
1211
|
+
return result
|
|
1212
|
+
else:
|
|
1213
|
+
result["display_type"] = "json"
|
|
1214
|
+
result["data"] = content
|
|
1215
|
+
return result
|
|
1216
|
+
|
|
1217
|
+
# Default: convert to string
|
|
1218
|
+
result["display_type"] = "text"
|
|
1219
|
+
result["data"] = str(content)
|
|
1220
|
+
return result
|
|
1221
|
+
|
|
1222
|
+
except Exception as e:
|
|
1223
|
+
result["status"] = "error"
|
|
1224
|
+
result["error"] = str(e)
|
|
1225
|
+
result["display_type"] = "error"
|
|
1226
|
+
result["data"] = f"Error processing content: {e}"
|
|
1227
|
+
return result
|
|
1228
|
+
|
|
1229
|
+
|
|
1230
|
+
def _is_file_path(content: str, workspace_root: Any) -> bool:
|
|
1231
|
+
"""Check if a string looks like a file path that exists."""
|
|
1232
|
+
if not content or len(content) > 500: # Too long to be a path
|
|
1233
|
+
return False
|
|
1234
|
+
if "\n" in content: # Contains newlines - not a path
|
|
1235
|
+
return False
|
|
1236
|
+
|
|
1237
|
+
# Check for common file extensions
|
|
1238
|
+
from pathlib import Path, PurePath
|
|
1239
|
+
ext = PurePath(content).suffix.lower()
|
|
1240
|
+
known_extensions = {
|
|
1241
|
+
'.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg', '.bmp', '.ico',
|
|
1242
|
+
'.html', '.htm',
|
|
1243
|
+
'.csv', '.tsv',
|
|
1244
|
+
'.json',
|
|
1245
|
+
'.pdf'
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
if ext in known_extensions:
|
|
1249
|
+
from .file_utils import _get_path
|
|
1250
|
+
from .virtual_fs import VirtualFilesystem
|
|
1251
|
+
|
|
1252
|
+
try:
|
|
1253
|
+
# For absolute paths with physical filesystem, check the path directly
|
|
1254
|
+
# This handles cases where an absolute path is passed that may be outside workspace
|
|
1255
|
+
if content.startswith('/') and not isinstance(workspace_root, VirtualFilesystem):
|
|
1256
|
+
abs_path = Path(content)
|
|
1257
|
+
return abs_path.exists() and abs_path.is_file()
|
|
1258
|
+
|
|
1259
|
+
# For relative paths or VirtualFilesystem, use _get_path
|
|
1260
|
+
full_path = _get_path(workspace_root, content)
|
|
1261
|
+
return full_path.exists()
|
|
1262
|
+
except Exception:
|
|
1263
|
+
return False
|
|
1264
|
+
|
|
1265
|
+
return False
|
|
1266
|
+
|
|
1267
|
+
|
|
1268
|
+
def _find_file_in_workspace(filename: str, workspace_root: Any) -> Optional[str]:
|
|
1269
|
+
"""Try to find a file in the workspace by searching common locations.
|
|
1270
|
+
|
|
1271
|
+
Args:
|
|
1272
|
+
filename: The filename or path to search for
|
|
1273
|
+
workspace_root: The workspace root (Path or VirtualFilesystem)
|
|
1274
|
+
|
|
1275
|
+
Returns:
|
|
1276
|
+
The relative path to the file if found, None otherwise
|
|
1277
|
+
"""
|
|
1278
|
+
from pathlib import PurePath
|
|
1279
|
+
from .file_utils import _get_path
|
|
1280
|
+
|
|
1281
|
+
# Extract just the filename if a path was provided
|
|
1282
|
+
basename = PurePath(filename).name
|
|
1283
|
+
|
|
1284
|
+
# Locations to search (in order of priority)
|
|
1285
|
+
search_paths = [
|
|
1286
|
+
filename, # As provided
|
|
1287
|
+
basename, # Just the filename in root
|
|
1288
|
+
f".canvas/{basename}", # In canvas folder
|
|
1289
|
+
f"output/{basename}", # Common output folder
|
|
1290
|
+
f"outputs/{basename}",
|
|
1291
|
+
f"results/{basename}",
|
|
1292
|
+
]
|
|
1293
|
+
|
|
1294
|
+
# Also try the exact path as-is first
|
|
1295
|
+
for path in search_paths:
|
|
1296
|
+
try:
|
|
1297
|
+
full_path = _get_path(workspace_root, path)
|
|
1298
|
+
if full_path.exists() and full_path.is_file():
|
|
1299
|
+
return path
|
|
1300
|
+
except Exception:
|
|
1301
|
+
continue
|
|
1302
|
+
|
|
1303
|
+
# Try recursive search for the filename (limited depth)
|
|
1304
|
+
try:
|
|
1305
|
+
from .virtual_fs import VirtualFilesystem
|
|
1306
|
+
if isinstance(workspace_root, VirtualFilesystem):
|
|
1307
|
+
# For VirtualFilesystem, check all files
|
|
1308
|
+
for file_path in workspace_root.glob("**/*"):
|
|
1309
|
+
if file_path.name == basename and file_path.is_file():
|
|
1310
|
+
# Return relative path
|
|
1311
|
+
return str(file_path).lstrip("/")
|
|
1312
|
+
else:
|
|
1313
|
+
# For physical filesystem
|
|
1314
|
+
import os
|
|
1315
|
+
for root, dirs, files in os.walk(workspace_root):
|
|
1316
|
+
# Limit depth to 3 levels
|
|
1317
|
+
rel_root = os.path.relpath(root, workspace_root)
|
|
1318
|
+
if rel_root.count(os.sep) > 3:
|
|
1319
|
+
continue
|
|
1320
|
+
# Skip hidden directories
|
|
1321
|
+
dirs[:] = [d for d in dirs if not d.startswith('.')]
|
|
1322
|
+
if basename in files:
|
|
1323
|
+
return os.path.relpath(os.path.join(root, basename), workspace_root)
|
|
1324
|
+
except Exception:
|
|
1325
|
+
pass
|
|
1326
|
+
|
|
1327
|
+
return None
|
|
1328
|
+
|
|
1329
|
+
|
|
1330
|
+
def _process_file_for_display(
|
|
1331
|
+
file_path: str,
|
|
1332
|
+
workspace_root: Any,
|
|
1333
|
+
title: Optional[str],
|
|
1334
|
+
display_type: Optional[str]
|
|
1335
|
+
) -> Dict[str, Any]:
|
|
1336
|
+
"""Process a file path and return display data."""
|
|
1337
|
+
import base64
|
|
1338
|
+
import json
|
|
1339
|
+
from pathlib import Path, PurePath
|
|
1340
|
+
from .file_utils import _get_path, read_file_content, get_file_download_data
|
|
1341
|
+
from .virtual_fs import VirtualFilesystem
|
|
1342
|
+
|
|
1343
|
+
result = {
|
|
1344
|
+
"type": "display_inline",
|
|
1345
|
+
"display_type": None,
|
|
1346
|
+
"title": title,
|
|
1347
|
+
"data": None,
|
|
1348
|
+
"preview": None,
|
|
1349
|
+
"filename": PurePath(file_path).name,
|
|
1350
|
+
"file_path": file_path,
|
|
1351
|
+
"downloadable": True,
|
|
1352
|
+
"status": "success",
|
|
1353
|
+
"error": None
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
ext = PurePath(file_path).suffix.lower()
|
|
1357
|
+
|
|
1358
|
+
# For absolute paths with physical filesystem, use the path directly
|
|
1359
|
+
is_absolute_physical = file_path.startswith('/') and not isinstance(workspace_root, VirtualFilesystem)
|
|
1360
|
+
if is_absolute_physical:
|
|
1361
|
+
full_path = Path(file_path)
|
|
1362
|
+
else:
|
|
1363
|
+
full_path = _get_path(workspace_root, file_path)
|
|
1364
|
+
|
|
1365
|
+
if not full_path.exists():
|
|
1366
|
+
result["status"] = "error"
|
|
1367
|
+
result["error"] = f"File not found: {file_path}"
|
|
1368
|
+
return result
|
|
1369
|
+
|
|
1370
|
+
# Image files
|
|
1371
|
+
image_exts = {'.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg', '.bmp', '.ico'}
|
|
1372
|
+
if ext in image_exts or display_type == "image":
|
|
1373
|
+
b64, filename, mime = get_file_download_data(workspace_root, file_path)
|
|
1374
|
+
if b64:
|
|
1375
|
+
result["display_type"] = "image"
|
|
1376
|
+
result["data"] = b64
|
|
1377
|
+
result["mime_type"] = mime
|
|
1378
|
+
return result
|
|
1379
|
+
|
|
1380
|
+
# HTML files
|
|
1381
|
+
if ext in {'.html', '.htm'} or display_type == "html":
|
|
1382
|
+
content, is_text, error = read_file_content(workspace_root, file_path)
|
|
1383
|
+
if content:
|
|
1384
|
+
result["display_type"] = "html"
|
|
1385
|
+
result["data"] = content
|
|
1386
|
+
result["preview"] = content[:1000] + "..." if len(content) > 1000 else content
|
|
1387
|
+
return result
|
|
1388
|
+
|
|
1389
|
+
# CSV files
|
|
1390
|
+
if ext in {'.csv', '.tsv'} or display_type in ("csv", "dataframe"):
|
|
1391
|
+
content, is_text, error = read_file_content(workspace_root, file_path)
|
|
1392
|
+
if content:
|
|
1393
|
+
try:
|
|
1394
|
+
import pandas as pd
|
|
1395
|
+
import io
|
|
1396
|
+
sep = '\t' if ext == '.tsv' else ','
|
|
1397
|
+
df = pd.read_csv(io.StringIO(content), sep=sep)
|
|
1398
|
+
df_result = _process_dataframe_for_display(df, title)
|
|
1399
|
+
df_result["filename"] = result["filename"]
|
|
1400
|
+
df_result["file_path"] = file_path
|
|
1401
|
+
df_result["downloadable"] = True
|
|
1402
|
+
return df_result
|
|
1403
|
+
except Exception as e:
|
|
1404
|
+
result["display_type"] = "text"
|
|
1405
|
+
result["data"] = content
|
|
1406
|
+
result["error"] = f"Could not parse as CSV: {e}"
|
|
1407
|
+
return result
|
|
1408
|
+
|
|
1409
|
+
# JSON files (check for Plotly)
|
|
1410
|
+
if ext == '.json' or display_type == "json":
|
|
1411
|
+
content, is_text, error = read_file_content(workspace_root, file_path)
|
|
1412
|
+
if content:
|
|
1413
|
+
try:
|
|
1414
|
+
data = json.loads(content)
|
|
1415
|
+
# Check if it's Plotly JSON
|
|
1416
|
+
if isinstance(data, dict) and 'data' in data and isinstance(data.get('data'), list):
|
|
1417
|
+
result["display_type"] = "plotly"
|
|
1418
|
+
result["data"] = data
|
|
1419
|
+
else:
|
|
1420
|
+
result["display_type"] = "json"
|
|
1421
|
+
result["data"] = data
|
|
1422
|
+
return result
|
|
1423
|
+
except json.JSONDecodeError:
|
|
1424
|
+
result["display_type"] = "text"
|
|
1425
|
+
result["data"] = content
|
|
1426
|
+
return result
|
|
1427
|
+
|
|
1428
|
+
# PDF files
|
|
1429
|
+
if ext == '.pdf':
|
|
1430
|
+
b64, filename, mime = get_file_download_data(workspace_root, file_path)
|
|
1431
|
+
if b64:
|
|
1432
|
+
result["display_type"] = "pdf"
|
|
1433
|
+
result["data"] = b64
|
|
1434
|
+
result["mime_type"] = mime
|
|
1435
|
+
return result
|
|
1436
|
+
|
|
1437
|
+
# Default: try to read as text
|
|
1438
|
+
content, is_text, error = read_file_content(workspace_root, file_path)
|
|
1439
|
+
if content:
|
|
1440
|
+
result["display_type"] = "text"
|
|
1441
|
+
result["data"] = content
|
|
1442
|
+
return result
|
|
1443
|
+
|
|
1444
|
+
result["status"] = "error"
|
|
1445
|
+
result["error"] = error or "Could not read file"
|
|
1446
|
+
return result
|
|
1447
|
+
|
|
1448
|
+
|
|
1449
|
+
def _process_dataframe_for_display(df: Any, title: Optional[str]) -> Dict[str, Any]:
|
|
1450
|
+
"""Process a pandas DataFrame for display."""
|
|
1451
|
+
result = {
|
|
1452
|
+
"type": "display_inline",
|
|
1453
|
+
"display_type": "dataframe",
|
|
1454
|
+
"title": title,
|
|
1455
|
+
"data": None,
|
|
1456
|
+
"preview": None,
|
|
1457
|
+
"downloadable": True,
|
|
1458
|
+
"status": "success",
|
|
1459
|
+
"error": None
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
# Create preview (first 10 rows)
|
|
1463
|
+
preview_df = df.head(10)
|
|
1464
|
+
result["preview"] = {
|
|
1465
|
+
"html": preview_df.to_html(index=False, classes="dataframe-table"),
|
|
1466
|
+
"rows_shown": len(preview_df),
|
|
1467
|
+
"total_rows": len(df),
|
|
1468
|
+
"columns": list(df.columns)
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
# Full data
|
|
1472
|
+
result["data"] = {
|
|
1473
|
+
"html": df.to_html(index=False, classes="dataframe-table"),
|
|
1474
|
+
"records": df.to_dict('records'),
|
|
1475
|
+
"columns": list(df.columns),
|
|
1476
|
+
"shape": list(df.shape)
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
# CSV for download
|
|
1480
|
+
result["csv"] = df.to_csv(index=False)
|
|
1481
|
+
|
|
1482
|
+
return result
|
|
1483
|
+
|
|
1484
|
+
|
|
944
1485
|
# =============================================================================
|
|
945
1486
|
# BASH TOOL
|
|
946
1487
|
# =============================================================================
|
|
@@ -951,7 +1492,8 @@ def bash(command: str, timeout: int = 60) -> Dict[str, Any]:
|
|
|
951
1492
|
Runs the command in the workspace directory. Use this for file operations,
|
|
952
1493
|
git commands, installing packages, or any shell operations.
|
|
953
1494
|
|
|
954
|
-
|
|
1495
|
+
In virtual filesystem mode (Linux only), commands run in a bubblewrap sandbox
|
|
1496
|
+
with network disabled for security.
|
|
955
1497
|
|
|
956
1498
|
Args:
|
|
957
1499
|
command: The bash command to execute
|
|
@@ -977,15 +1519,9 @@ def bash(command: str, timeout: int = 60) -> Dict[str, Any]:
|
|
|
977
1519
|
# Run a script
|
|
978
1520
|
bash("python script.py")
|
|
979
1521
|
"""
|
|
980
|
-
#
|
|
1522
|
+
# In virtual filesystem mode, use sandboxed execution
|
|
981
1523
|
if VIRTUAL_FS:
|
|
982
|
-
return
|
|
983
|
-
"stdout": "",
|
|
984
|
-
"stderr": "Bash commands are disabled in virtual filesystem mode for security reasons. "
|
|
985
|
-
"Use the built-in file tools (read_file, write_file, list_directory) instead.",
|
|
986
|
-
"return_code": 1,
|
|
987
|
-
"status": "error"
|
|
988
|
-
}
|
|
1524
|
+
return _bash_sandboxed(command, timeout)
|
|
989
1525
|
|
|
990
1526
|
try:
|
|
991
1527
|
result = subprocess.run(
|
|
@@ -1018,3 +1554,69 @@ def bash(command: str, timeout: int = 60) -> Dict[str, Any]:
|
|
|
1018
1554
|
"return_code": -1,
|
|
1019
1555
|
"status": "error"
|
|
1020
1556
|
}
|
|
1557
|
+
|
|
1558
|
+
|
|
1559
|
+
def _bash_sandboxed(command: str, timeout: int = 60) -> Dict[str, Any]:
|
|
1560
|
+
"""Execute bash command in sandboxed environment for virtual FS mode.
|
|
1561
|
+
|
|
1562
|
+
Uses bubblewrap (Linux) for sandboxing with:
|
|
1563
|
+
- No network access
|
|
1564
|
+
- Isolated PID namespace
|
|
1565
|
+
- Read-only system directories
|
|
1566
|
+
- Writable workspace directory synced with VirtualFilesystem
|
|
1567
|
+
"""
|
|
1568
|
+
from .sandbox import get_executor_for_session, get_available_sandbox
|
|
1569
|
+
from .virtual_fs import get_session_manager
|
|
1570
|
+
|
|
1571
|
+
# Get current session context
|
|
1572
|
+
session_id = get_tool_session_context()
|
|
1573
|
+
if not session_id:
|
|
1574
|
+
return {
|
|
1575
|
+
"stdout": "",
|
|
1576
|
+
"stderr": "No session context available for sandboxed execution",
|
|
1577
|
+
"return_code": 1,
|
|
1578
|
+
"status": "error"
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
# Check if sandbox is available
|
|
1582
|
+
sandbox = get_available_sandbox()
|
|
1583
|
+
if sandbox is None:
|
|
1584
|
+
return {
|
|
1585
|
+
"stdout": "",
|
|
1586
|
+
"stderr": "Bash commands require bubblewrap (bwrap) or Docker in virtual filesystem mode. "
|
|
1587
|
+
"Install bubblewrap: apt-get install bubblewrap",
|
|
1588
|
+
"return_code": 1,
|
|
1589
|
+
"status": "error"
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
# Get the virtual filesystem for this session
|
|
1593
|
+
fs = get_session_manager().get_filesystem(session_id)
|
|
1594
|
+
if fs is None:
|
|
1595
|
+
return {
|
|
1596
|
+
"stdout": "",
|
|
1597
|
+
"stderr": "Session filesystem not found",
|
|
1598
|
+
"return_code": 1,
|
|
1599
|
+
"status": "error"
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
# Get or create executor for this session
|
|
1603
|
+
executor = get_executor_for_session(session_id, fs)
|
|
1604
|
+
|
|
1605
|
+
# Execute command in sandbox
|
|
1606
|
+
return executor.execute(command, timeout=timeout)
|
|
1607
|
+
|
|
1608
|
+
|
|
1609
|
+
# Add a think tool
|
|
1610
|
+
def think_tool(reflection: str) -> str:
|
|
1611
|
+
"""A tool to reflect on your actions and reasoning.
|
|
1612
|
+
|
|
1613
|
+
This tool allows you to pause and think about your next steps,
|
|
1614
|
+
evaluate your current state, or reconsider your approach. Use
|
|
1615
|
+
this tool to generate internal reflections that the user can see.
|
|
1616
|
+
|
|
1617
|
+
Args:
|
|
1618
|
+
reflection: The reflection text
|
|
1619
|
+
Returns:
|
|
1620
|
+
str: The recorded reflection
|
|
1621
|
+
"""
|
|
1622
|
+
return reflection
|