NeerajCodz commited on
Commit
64ae2f9
·
1 Parent(s): 1f42d7b

feat: add tool call visibility in step accordion

Browse files

- emit tool_call step events for browser.navigate, html.parse, html.select, csv.generate
- show tool name, description, parameters, and result in frontend accordion
- add Zap icon for tool_call action with yellow highlight
- display tool call details section with structured parameter/result views

This enables users to see exactly which tools agents invoke during scraping.

backend/app/api/routes/scrape.py CHANGED
@@ -929,15 +929,21 @@ async def _scrape_github_trending(
929
  # Navigate to GitHub trending
930
  trending_url = "https://github.com/trending"
931
 
 
932
  step_num += 1
933
  yield _record_step(
934
  session,
935
  ScrapeStep(
936
  step_number=step_num,
937
- action="navigate",
938
  url=trending_url,
939
  status="running",
940
- message="Navigating to GitHub trending page...",
 
 
 
 
 
941
  timestamp=_now_iso(),
942
  ),
943
  )
@@ -954,6 +960,28 @@ async def _scrape_github_trending(
954
  nav_reward = 0.5 if nav_obs.page_html else 0.0
955
  total_reward += nav_reward
956
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
957
  # Update the navigation step with actual reward
958
  step_num += 1
959
  yield _record_step(
@@ -962,8 +990,8 @@ async def _scrape_github_trending(
962
  step_number=step_num,
963
  action="navigate",
964
  url=trending_url,
965
- status="completed" if nav_obs.page_html else "failed",
966
- message=f"Navigated to {trending_url}" if nav_obs.page_html else "Navigation failed",
967
  reward=nav_reward,
968
  duration_ms=nav_info.get("step_duration_ms", 0),
969
  timestamp=_now_iso(),
@@ -974,9 +1002,83 @@ async def _scrape_github_trending(
974
  session["errors"].append("Failed to load GitHub trending page")
975
  return
976
 
977
- # Parse trending repos from HTML
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
978
  soup = parse_html(nav_obs.page_html)
979
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
980
  step_num += 1
981
  yield _record_step(
982
  session,
@@ -991,9 +1093,6 @@ async def _scrape_github_trending(
991
  ),
992
  )
993
 
994
- # Find repository entries (GitHub trending structure)
995
- repo_articles = soup.find_all("article", class_="Box-row") or soup.find_all("div", class_="Box-row")
996
-
997
  for article in repo_articles[:20]: # Limit to first 20
998
  try:
999
  # Extract repo name and username
@@ -1055,6 +1154,28 @@ async def _scrape_github_trending(
1055
  ),
1056
  )
1057
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1058
  # Generate clean CSV output
1059
  csv_buffer = io.StringIO()
1060
  writer = csv.DictWriter(csv_buffer, fieldnames=["username", "repo_name", "stars", "forks"])
@@ -1062,6 +1183,27 @@ async def _scrape_github_trending(
1062
  writer.writerows(trending_repos)
1063
  clean_csv = csv_buffer.getvalue()
1064
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1065
  # Store the clean CSV directly as extracted data for CSV output format
1066
  if request.output_format == OutputFormat.CSV:
1067
  session["extracted_data"] = {
@@ -1565,6 +1707,25 @@ async def _scrape_single_page(
1565
  ),
1566
  )
1567
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1568
  navigate_action = Action(
1569
  action_type=ActionType.NAVIGATE,
1570
  parameters={"url": url},
@@ -1575,6 +1736,23 @@ async def _scrape_single_page(
1575
 
1576
  nav_success = nav_info.get("action_result", {}).get("success", bool(nav_obs.page_html))
1577
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1578
  yield _record_step(
1579
  session,
1580
  ScrapeStep(
@@ -1598,14 +1776,20 @@ async def _scrape_single_page(
1598
 
1599
  for field_name in fields_to_extract:
1600
  step_num += 1
 
1601
  yield _record_step(
1602
  session,
1603
  ScrapeStep(
1604
  step_number=step_num,
1605
- action="extract",
1606
  url=url,
1607
  status="running",
1608
- message=f"Extracting {field_name}...",
 
 
 
 
 
1609
  timestamp=_now_iso(),
1610
  ),
1611
  )
@@ -1624,6 +1808,24 @@ async def _scrape_single_page(
1624
  extracted[field_name] = ef.value
1625
  break
1626
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1627
  yield _record_step(
1628
  session,
1629
  ScrapeStep(
 
929
  # Navigate to GitHub trending
930
  trending_url = "https://github.com/trending"
931
 
932
+ # Tool call: browser.navigate
933
  step_num += 1
934
  yield _record_step(
935
  session,
936
  ScrapeStep(
937
  step_number=step_num,
938
+ action="tool_call",
939
  url=trending_url,
940
  status="running",
941
+ message=f"browser.navigate(url='{trending_url}')",
942
+ extracted_data={
943
+ "tool_name": "browser.navigate",
944
+ "tool_description": "Navigate browser to GitHub trending page",
945
+ "parameters": {"url": trending_url, "wait_for": "page_load"},
946
+ },
947
  timestamp=_now_iso(),
948
  ),
949
  )
 
960
  nav_reward = 0.5 if nav_obs.page_html else 0.0
961
  total_reward += nav_reward
962
 
963
+ nav_success = bool(nav_obs.page_html)
964
+ yield _record_step(
965
+ session,
966
+ ScrapeStep(
967
+ step_number=step_num,
968
+ action="tool_call",
969
+ url=trending_url,
970
+ status="completed" if nav_success else "failed",
971
+ message=f"browser.navigate() → {len(nav_obs.page_html) if nav_obs.page_html else 0} bytes",
972
+ reward=0.1,
973
+ extracted_data={
974
+ "tool_name": "browser.navigate",
975
+ "result": {
976
+ "success": nav_success,
977
+ "html_length": len(nav_obs.page_html) if nav_obs.page_html else 0,
978
+ "status_code": 200 if nav_success else 0,
979
+ },
980
+ },
981
+ timestamp=_now_iso(),
982
+ ),
983
+ )
984
+
985
  # Update the navigation step with actual reward
986
  step_num += 1
987
  yield _record_step(
 
990
  step_number=step_num,
991
  action="navigate",
992
  url=trending_url,
993
+ status="completed" if nav_success else "failed",
994
+ message=f"Navigated to {trending_url}" if nav_success else "Navigation failed",
995
  reward=nav_reward,
996
  duration_ms=nav_info.get("step_duration_ms", 0),
997
  timestamp=_now_iso(),
 
1002
  session["errors"].append("Failed to load GitHub trending page")
1003
  return
1004
 
1005
+ # Tool call: html.parse
1006
+ step_num += 1
1007
+ yield _record_step(
1008
+ session,
1009
+ ScrapeStep(
1010
+ step_number=step_num,
1011
+ action="tool_call",
1012
+ url=trending_url,
1013
+ status="running",
1014
+ message="html.parse(content)",
1015
+ extracted_data={
1016
+ "tool_name": "html.parse",
1017
+ "tool_description": "Parse HTML document into structured DOM",
1018
+ "parameters": {"parser": "html.parser", "content_length": len(nav_obs.page_html)},
1019
+ },
1020
+ timestamp=_now_iso(),
1021
+ ),
1022
+ )
1023
+
1024
  soup = parse_html(nav_obs.page_html)
1025
 
1026
+ yield _record_step(
1027
+ session,
1028
+ ScrapeStep(
1029
+ step_number=step_num,
1030
+ action="tool_call",
1031
+ url=trending_url,
1032
+ status="completed",
1033
+ message="html.parse() → DOM ready",
1034
+ reward=0.05,
1035
+ extracted_data={
1036
+ "tool_name": "html.parse",
1037
+ "result": {"parsed": True, "soup_type": "BeautifulSoup"},
1038
+ },
1039
+ timestamp=_now_iso(),
1040
+ ),
1041
+ )
1042
+
1043
+ # Tool call: html.select
1044
+ step_num += 1
1045
+ yield _record_step(
1046
+ session,
1047
+ ScrapeStep(
1048
+ step_number=step_num,
1049
+ action="tool_call",
1050
+ url=trending_url,
1051
+ status="running",
1052
+ message="html.select(selector='article.Box-row')",
1053
+ extracted_data={
1054
+ "tool_name": "html.select",
1055
+ "tool_description": "Select repository elements from trending page",
1056
+ "parameters": {"selector": "article.Box-row", "fallback": "div.Box-row"},
1057
+ },
1058
+ timestamp=_now_iso(),
1059
+ ),
1060
+ )
1061
+
1062
+ # Find repository entries (GitHub trending structure)
1063
+ repo_articles = soup.find_all("article", class_="Box-row") or soup.find_all("div", class_="Box-row")
1064
+
1065
+ yield _record_step(
1066
+ session,
1067
+ ScrapeStep(
1068
+ step_number=step_num,
1069
+ action="tool_call",
1070
+ url=trending_url,
1071
+ status="completed",
1072
+ message=f"html.select() → {len(repo_articles)} elements",
1073
+ reward=0.1,
1074
+ extracted_data={
1075
+ "tool_name": "html.select",
1076
+ "result": {"elements_found": len(repo_articles), "selector_used": "article.Box-row"},
1077
+ },
1078
+ timestamp=_now_iso(),
1079
+ ),
1080
+ )
1081
+
1082
  step_num += 1
1083
  yield _record_step(
1084
  session,
 
1093
  ),
1094
  )
1095
 
 
 
 
1096
  for article in repo_articles[:20]: # Limit to first 20
1097
  try:
1098
  # Extract repo name and username
 
1154
  ),
1155
  )
1156
 
1157
+ # Tool call: csv.generate
1158
+ step_num += 1
1159
+ yield _record_step(
1160
+ session,
1161
+ ScrapeStep(
1162
+ step_number=step_num,
1163
+ action="tool_call",
1164
+ url=trending_url,
1165
+ status="running",
1166
+ message="csv.generate(data, fields=['username', 'repo_name', 'stars', 'forks'])",
1167
+ extracted_data={
1168
+ "tool_name": "csv.generate",
1169
+ "tool_description": "Generate CSV output from repository data",
1170
+ "parameters": {
1171
+ "fields": ["username", "repo_name", "stars", "forks"],
1172
+ "row_count": len(trending_repos),
1173
+ },
1174
+ },
1175
+ timestamp=_now_iso(),
1176
+ ),
1177
+ )
1178
+
1179
  # Generate clean CSV output
1180
  csv_buffer = io.StringIO()
1181
  writer = csv.DictWriter(csv_buffer, fieldnames=["username", "repo_name", "stars", "forks"])
 
1183
  writer.writerows(trending_repos)
1184
  clean_csv = csv_buffer.getvalue()
1185
 
1186
+ yield _record_step(
1187
+ session,
1188
+ ScrapeStep(
1189
+ step_number=step_num,
1190
+ action="tool_call",
1191
+ url=trending_url,
1192
+ status="completed",
1193
+ message=f"csv.generate() → {len(clean_csv)} bytes",
1194
+ reward=0.1,
1195
+ extracted_data={
1196
+ "tool_name": "csv.generate",
1197
+ "result": {
1198
+ "csv_length": len(clean_csv),
1199
+ "rows": len(trending_repos),
1200
+ "columns": 4,
1201
+ },
1202
+ },
1203
+ timestamp=_now_iso(),
1204
+ ),
1205
+ )
1206
+
1207
  # Store the clean CSV directly as extracted data for CSV output format
1208
  if request.output_format == OutputFormat.CSV:
1209
  session["extracted_data"] = {
 
1707
  ),
1708
  )
1709
 
1710
+ # Tool call: browser.navigate
1711
+ step_num += 1
1712
+ yield _record_step(
1713
+ session,
1714
+ ScrapeStep(
1715
+ step_number=step_num,
1716
+ action="tool_call",
1717
+ url=url,
1718
+ status="running",
1719
+ message="browser.navigate(url)",
1720
+ extracted_data={
1721
+ "tool_name": "browser.navigate",
1722
+ "tool_description": "Navigate browser to target URL",
1723
+ "parameters": {"url": url},
1724
+ },
1725
+ timestamp=_now_iso(),
1726
+ ),
1727
+ )
1728
+
1729
  navigate_action = Action(
1730
  action_type=ActionType.NAVIGATE,
1731
  parameters={"url": url},
 
1736
 
1737
  nav_success = nav_info.get("action_result", {}).get("success", bool(nav_obs.page_html))
1738
 
1739
+ yield _record_step(
1740
+ session,
1741
+ ScrapeStep(
1742
+ step_number=step_num,
1743
+ action="tool_call",
1744
+ url=url,
1745
+ status="completed" if nav_success else "failed",
1746
+ message="browser.navigate(url) → success" if nav_success else "browser.navigate(url) → failed",
1747
+ reward=0.05,
1748
+ extracted_data={
1749
+ "tool_name": "browser.navigate",
1750
+ "result": {"success": nav_success, "html_length": len(nav_obs.page_html) if nav_obs.page_html else 0},
1751
+ },
1752
+ timestamp=_now_iso(),
1753
+ ),
1754
+ )
1755
+
1756
  yield _record_step(
1757
  session,
1758
  ScrapeStep(
 
1776
 
1777
  for field_name in fields_to_extract:
1778
  step_num += 1
1779
+ # Tool call: html.extract
1780
  yield _record_step(
1781
  session,
1782
  ScrapeStep(
1783
  step_number=step_num,
1784
+ action="tool_call",
1785
  url=url,
1786
  status="running",
1787
+ message=f"html.extract(field='{field_name}')",
1788
+ extracted_data={
1789
+ "tool_name": "html.extract",
1790
+ "tool_description": f"Extract {field_name} from HTML document",
1791
+ "parameters": {"field_name": field_name},
1792
+ },
1793
  timestamp=_now_iso(),
1794
  ),
1795
  )
 
1808
  extracted[field_name] = ef.value
1809
  break
1810
 
1811
+ value_preview = str(extracted.get(field_name, ""))[:100]
1812
+ yield _record_step(
1813
+ session,
1814
+ ScrapeStep(
1815
+ step_number=step_num,
1816
+ action="tool_call",
1817
+ url=url,
1818
+ status="completed",
1819
+ message=f"html.extract(field='{field_name}') → {value_preview}",
1820
+ reward=0.05,
1821
+ extracted_data={
1822
+ "tool_name": "html.extract",
1823
+ "result": {field_name: extracted.get(field_name)},
1824
+ },
1825
+ timestamp=_now_iso(),
1826
+ ),
1827
+ )
1828
+
1829
  yield _record_step(
1830
  session,
1831
  ScrapeStep(
frontend/src/components/Dashboard.tsx CHANGED
@@ -64,6 +64,8 @@ const getStepIcon = (action: string): LucideIcon => {
64
  'complete': CheckCircle,
65
  'mcp_search': Search,
66
  'python_sandbox': Terminal,
 
 
67
  'error': XCircle,
68
  };
69
  return iconMap[action] || Activity;
@@ -89,6 +91,8 @@ const getStepColor = (action: string, status: string): string => {
89
  'complete': 'text-green-400 bg-green-500/20 border-green-500/30',
90
  'mcp_search': 'text-cyan-400 bg-cyan-500/20 border-cyan-500/30',
91
  'python_sandbox': 'text-yellow-400 bg-yellow-500/20 border-yellow-500/30',
 
 
92
  };
93
  return colorMap[action] || 'text-slate-400 bg-slate-500/20 border-slate-500/30';
94
  };
@@ -110,6 +114,13 @@ const StepAccordionItem: React.FC<StepAccordionItemProps> = ({ step, isExpanded,
110
  const Icon = getStepIcon(step.action);
111
  const colorClasses = getStepColor(step.action, step.status);
112
 
 
 
 
 
 
 
 
113
  return (
114
  <div className={classNames(
115
  'border rounded-lg overflow-hidden transition-all',
@@ -120,14 +131,14 @@ const StepAccordionItem: React.FC<StepAccordionItemProps> = ({ step, isExpanded,
120
  onClick={onToggle}
121
  className="w-full flex items-center justify-between px-4 py-3 hover:bg-white/5 transition-colors"
122
  >
123
- <div className="flex items-center gap-3">
124
- <div className={classNames('p-2 rounded-lg', colorClasses.split(' ').slice(1, 3).join(' '))}>
125
  <Icon className={classNames('w-4 h-4', colorClasses.split(' ')[0])} />
126
  </div>
127
- <div className="text-left">
128
  <div className="flex items-center gap-2">
129
  <span className="text-sm font-medium text-white">
130
- {step.action.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase())}
131
  </span>
132
  <Badge
133
  variant={step.status === 'completed' ? 'success' : step.status === 'failed' ? 'error' : 'info'}
@@ -136,10 +147,13 @@ const StepAccordionItem: React.FC<StepAccordionItemProps> = ({ step, isExpanded,
136
  {step.status}
137
  </Badge>
138
  </div>
139
- <p className="text-xs text-slate-400 truncate max-w-[300px]">{step.message}</p>
 
 
 
140
  </div>
141
  </div>
142
- <div className="flex items-center gap-3">
143
  <div className="text-right">
144
  <span className="text-xs text-slate-500">Step {step.step_number}</span>
145
  {step.reward > 0 && (
@@ -156,6 +170,42 @@ const StepAccordionItem: React.FC<StepAccordionItemProps> = ({ step, isExpanded,
156
 
157
  {isExpanded && (
158
  <div className="px-4 py-3 border-t border-white/10 bg-slate-900/50 space-y-3">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
  {/* Step Details */}
160
  <div className="grid grid-cols-2 gap-4 text-xs">
161
  <div>
@@ -188,8 +238,8 @@ const StepAccordionItem: React.FC<StepAccordionItemProps> = ({ step, isExpanded,
188
  </div>
189
  </div>
190
 
191
- {/* Extracted Data */}
192
- {step.extracted_data && Object.keys(step.extracted_data).length > 0 && (
193
  <div className="mt-3">
194
  <p className="text-xs text-slate-500 mb-2">Extracted Data:</p>
195
  <pre className="text-xs text-slate-300 bg-slate-800/50 rounded-lg p-3 overflow-auto max-h-40 font-mono">
 
64
  'complete': CheckCircle,
65
  'mcp_search': Search,
66
  'python_sandbox': Terminal,
67
+ 'site_template': FileText,
68
+ 'tool_call': Zap,
69
  'error': XCircle,
70
  };
71
  return iconMap[action] || Activity;
 
91
  'complete': 'text-green-400 bg-green-500/20 border-green-500/30',
92
  'mcp_search': 'text-cyan-400 bg-cyan-500/20 border-cyan-500/30',
93
  'python_sandbox': 'text-yellow-400 bg-yellow-500/20 border-yellow-500/30',
94
+ 'site_template': 'text-violet-400 bg-violet-500/20 border-violet-500/30',
95
+ 'tool_call': 'text-yellow-300 bg-yellow-500/20 border-yellow-500/30',
96
  };
97
  return colorMap[action] || 'text-slate-400 bg-slate-500/20 border-slate-500/30';
98
  };
 
114
  const Icon = getStepIcon(step.action);
115
  const colorClasses = getStepColor(step.action, step.status);
116
 
117
+ // Check if this is a tool call
118
+ const isToolCall = step.action === 'tool_call';
119
+ const toolName = (step.extracted_data?.tool_name as string) || '';
120
+ const toolDescription = (step.extracted_data?.tool_description as string) || '';
121
+ const toolParameters = (step.extracted_data?.parameters as Record<string, any>) || {};
122
+ const toolResult = (step.extracted_data?.result as Record<string, any>) || {};
123
+
124
  return (
125
  <div className={classNames(
126
  'border rounded-lg overflow-hidden transition-all',
 
131
  onClick={onToggle}
132
  className="w-full flex items-center justify-between px-4 py-3 hover:bg-white/5 transition-colors"
133
  >
134
+ <div className="flex items-center gap-3 flex-1 min-w-0">
135
+ <div className={classNames('p-2 rounded-lg flex-shrink-0', colorClasses.split(' ').slice(1, 3).join(' '))}>
136
  <Icon className={classNames('w-4 h-4', colorClasses.split(' ')[0])} />
137
  </div>
138
+ <div className="text-left min-w-0 flex-1">
139
  <div className="flex items-center gap-2">
140
  <span className="text-sm font-medium text-white">
141
+ {isToolCall ? toolName : step.action.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase())}
142
  </span>
143
  <Badge
144
  variant={step.status === 'completed' ? 'success' : step.status === 'failed' ? 'error' : 'info'}
 
147
  {step.status}
148
  </Badge>
149
  </div>
150
+ <p className="text-xs text-slate-400 truncate">{step.message}</p>
151
+ {isToolCall && toolDescription && (
152
+ <p className="text-[10px] text-slate-500 mt-0.5">{toolDescription}</p>
153
+ )}
154
  </div>
155
  </div>
156
+ <div className="flex items-center gap-3 flex-shrink-0">
157
  <div className="text-right">
158
  <span className="text-xs text-slate-500">Step {step.step_number}</span>
159
  {step.reward > 0 && (
 
170
 
171
  {isExpanded && (
172
  <div className="px-4 py-3 border-t border-white/10 bg-slate-900/50 space-y-3">
173
+ {/* Tool Call Specific Details */}
174
+ {isToolCall && (
175
+ <>
176
+ <div className="bg-slate-800/50 rounded-lg p-3 space-y-2">
177
+ <div className="flex items-center gap-2">
178
+ <Zap className="w-3 h-3 text-yellow-400" />
179
+ <span className="text-xs font-semibold text-yellow-400">Tool Call Details</span>
180
+ </div>
181
+
182
+ {toolDescription && (
183
+ <div className="text-xs text-slate-300">
184
+ <span className="text-slate-500">Description:</span> {toolDescription}
185
+ </div>
186
+ )}
187
+
188
+ {Object.keys(toolParameters).length > 0 && (
189
+ <div>
190
+ <span className="text-xs text-slate-500">Parameters:</span>
191
+ <pre className="text-xs text-cyan-300 bg-slate-900/70 rounded p-2 mt-1 overflow-auto max-h-20 font-mono">
192
+ {JSON.stringify(toolParameters, null, 2)}
193
+ </pre>
194
+ </div>
195
+ )}
196
+
197
+ {Object.keys(toolResult).length > 0 && (
198
+ <div>
199
+ <span className="text-xs text-slate-500">Result:</span>
200
+ <pre className="text-xs text-emerald-300 bg-slate-900/70 rounded p-2 mt-1 overflow-auto max-h-20 font-mono">
201
+ {JSON.stringify(toolResult, null, 2)}
202
+ </pre>
203
+ </div>
204
+ )}
205
+ </div>
206
+ </>
207
+ )}
208
+
209
  {/* Step Details */}
210
  <div className="grid grid-cols-2 gap-4 text-xs">
211
  <div>
 
238
  </div>
239
  </div>
240
 
241
+ {/* Extracted Data (non-tool calls or if additional data exists) */}
242
+ {step.extracted_data && Object.keys(step.extracted_data).length > 0 && !isToolCall && (
243
  <div className="mt-3">
244
  <p className="text-xs text-slate-500 mb-2">Extracted Data:</p>
245
  <pre className="text-xs text-slate-300 bg-slate-800/50 rounded-lg p-3 overflow-auto max-h-40 font-mono">