Spaces:
Sleeping
Sleeping
File size: 66,462 Bytes
8edee29 bc7e224 8edee29 bc7e224 8edee29 bc7e224 8edee29 bc7e224 8edee29 bc7e224 8edee29 bc7e224 8edee29 bc7e224 8edee29 bc7e224 8edee29 bc7e224 8edee29 bc7e224 8edee29 bc7e224 8edee29 bc7e224 8edee29 bc7e224 8edee29 bc7e224 8edee29 bc7e224 8edee29 bc7e224 8edee29 bc7e224 8edee29 bc7e224 8edee29 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525 1526 1527 1528 1529 1530 1531 | """
app.py
------
AutoDevAgent β Gradio UI entry point.
This is the only file that imports from the UI layer and wires all
components together. It assembles the Gradio Blocks layout, registers
event handlers, and launches the app for HuggingFace Spaces.
Layout:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β AutoDevAgent β
β ββββββββββββββββββββ ββββββββββββββββββββββββββββ β
β β Task input β β Pipeline Visualiser β β
β β Language select β β (fixed nodes, live β β
β β Model switcher β β animated arrows) β β
β β Run / Benchmark β ββββββββββββββββββββββββββββ β
β ββββββββββββββββββββ β
β ββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Output tabs: β β
β β Code | Tests | Explanation | Flowchart β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββ β
β ββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Self-reflection accordion β Stats strip β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββ β
β ββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β HITL panel (visible only on max retries) β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββ β
β ββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Session history (collapsible) β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Usage:
python app.py # local dev
gradio app.py # HuggingFace Spaces (auto-detected)
"""
from dotenv import load_dotenv
load_dotenv() # load .env before any other imports read env vars
import logging
import time
import gradio as gr
from config import settings
from pipeline.graph import build_graph
from pipeline.state import Language, PipelineStatus, PipelineState
from agents.detect_agent import DetectAgent
from ui.components import (
build_hitl_panel,
build_clarification_panel,
build_reflection_panel,
build_stats_strip,
build_model_switcher,
build_language_selector,
update_hitl_panel,
update_reflection_panel,
update_stats_strip,
update_language_badge,
)
from ui.pipeline_visualiser import (
get_pipeline_html,
get_pipeline_html_with_update,
get_status_update_js,
STATUS_TO_ACTIVE_NODE,
)
from ui.session_history import SessionHistory
from observability.wandb_tracker import WandbTracker
from evaluation.benchmark import BenchmarkRunner
from evaluation.metrics import compute_metrics, format_results_for_display
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# ------------------------------------------------------------------ #
# Example prompts per language #
# ------------------------------------------------------------------ #
PYTHON_EXAMPLES = [
"Write a recursive function to compute the nth Fibonacci number. Then add memoization to make it efficient for n=100",
"Write a function that takes a list of integers and returns the longest strictly increasing subsequence",
"Create a classBankAccountwith deposit, withdraw, and get_balance methods. Raise an exception if withdrawal exceeds balance",
"Write a function that checks if two strings are anagrams (case-insensitive, ignore spaces and punctuation)",
]
SQL_EXAMPLES = [
"Write a query to find the second highest salary from an employees table. Assume no schema is given - infer it",
"Calculate the running total of sales per product over time. Use a sales table with columns: product_id, sale_date, amount",
"Find customers who have placed orders in the last 30 days but not in the previous 30 days (newly active customers)",
"Write a query to de-duplicate a tableuser_eventskeeping only the latest event per user based on event_time",
]
# ------------------------------------------------------------------ #
# Mermaid renderer #
# ------------------------------------------------------------------ #
def _parse_test_docstrings(test_code: str) -> dict[str, str]:
"""
Extract {method_name: docstring} from a unittest file.
Handles both single and triple-quoted docstrings.
"""
import re
result = {}
# Match: def test_foo(self): followed by an optional docstring
pattern = re.compile(
r'def\s+(test_\w+)\s*\(self[^)]*\)\s*:\s*\n'
r'\s*(?:"""(.*?)"""|\'\'\'(.*?)\'\'\')',
re.DOTALL,
)
for m in pattern.finditer(test_code or ""):
name = m.group(1)
doc = (m.group(2) or m.group(3) or "").strip()
doc = " ".join(doc.split()) # collapse whitespace
result[name] = doc
return result
def _build_test_summary(test_result, test_code: str = "") -> str:
"""Return a markdown summary of test results for display in the Tests tab."""
if test_result is None:
return ""
total = test_result.total_tests
passed = test_result.passed_tests
failed = test_result.failed_tests
icon = "β
" if failed == 0 else "β οΈ"
docstrings = _parse_test_docstrings(test_code)
# Failed test names extracted from failure messages
import re
failed_names = set()
for f in test_result.failures:
m = re.search(r'(test_\w+)', f)
if m:
failed_names.add(m.group(1))
_summary_color = "#22C55E" if failed == 0 else "#F59E0B"
lines = [f'{icon} <span style="color:{_summary_color};">**{passed}/{total} tests passed**</span>\n']
# Per-test breakdown
for name, doc in docstrings.items():
status = "β" if name in failed_names else "β
"
desc = f" β *{doc}*" if doc else ""
lines.append(f"{status} `{name}`{desc}")
if failed > 0:
lines.append(f"\n**{failed} failure detail(s):**")
for f in test_result.failures:
lines.append(f"```\n{f.strip()}\n```")
return "\n\n".join(lines)
def _generate_report(task: str, lang: str, final_state, elapsed: float) -> str:
"""
Build a full markdown report for download.
Includes task, code, test results with per-test descriptions, explanation, and run stats.
"""
import re
tr = final_state.test_result
test_code = final_state.generated_tests or ""
test_lines = []
if tr:
test_lines += [
f"- **Total:** {tr.total_tests}",
f"- **Passed:** {tr.passed_tests}",
f"- **Failed:** {tr.failed_tests}",
]
# Per-test breakdown with docstrings
docstrings = _parse_test_docstrings(test_code)
failed_names = set()
for f in tr.failures:
m = re.search(r'(test_\w+)', f)
if m:
failed_names.add(m.group(1))
if docstrings:
test_lines.append("\n**Per-test breakdown:**")
for name, doc in docstrings.items():
status = "β" if name in failed_names else "β
"
desc = f": {doc}" if doc else ""
test_lines.append(f"- {status} `{name}`{desc}")
if tr.failures:
test_lines.append("\n**Failure details:**")
for f in tr.failures:
test_lines.append(f"```\n{f.strip()}\n```")
else:
test_lines = ["*No test results available.*"]
code_lang = lang if lang in ("python", "sql") else "python"
code_block = f"```{code_lang}\n{final_state.final_code()}\n```"
tests_block = f"```python\n{final_state.generated_tests or ''}\n```"
tokens = final_state.token_usage.total_tokens
iters = final_state.debug_iterations
status = final_state.status.value
sections = [
"# AutoDevAgent β Run Report",
f"## Task\n{task}",
f"## Language\n`{lang}`",
f"## Generated Code\n{code_block}",
"## Test Results\n" + "\n".join(test_lines),
f"## Test Code\n{tests_block}",
f"## Explanation\n{final_state.explanation or '*Not generated.*'}",
(
"## Run Stats\n"
f"- **Status:** {status}\n"
f"- **Debug iterations:** {iters}\n"
f"- **Total tokens:** {tokens:,}\n"
f"- **Elapsed time:** {elapsed:.1f}s"
),
]
return "\n\n---\n\n".join(sections)
def _render_mermaid(raw: str) -> str:
"""
Parse-then-rebuild Mermaid renderer (strict simple mode).
Parses whatever the LLM produced into nodes + edges, then rebuilds
100% clean Mermaid using only the safest subset:
- graph TD header
- rectangle nodes only A["label"]
- plain arrows only A --> B (no edge labels, no diamonds)
- no back-edges (DFS cycle removal)
- max 10 nodes (truncated if LLM went over)
This means any LLM syntax error, diamond node, edge label, pipe
character, or loop can NEVER reach the Mermaid renderer.
"""
import re
if not raw or not raw.strip():
return "_No flowchart generated for this task._"
code = raw.strip()
# ββ Strip markdown fences βββββββββββββββββββββββββββββββββββββββ #
if code.startswith("```"):
lines = code.splitlines()
end = len(lines) - 1 if lines[-1].strip() == "```" else len(lines)
code = "\n".join(lines[1:end]).strip()
# ββ Handle single-line LLM output ββββββββββββββββββββββββββββββ #
if "\n" not in code:
code = re.sub(r'(?<=[}\]\)]) (?=[A-Za-z])', '\n ', code)
lines = code.splitlines()
# ββ Label cleaner βββββββββββββββββββββββββββββββββββββββββββββββ #
# Remove ALL characters that can break Mermaid inside a label.
_UNSAFE_RE = re.compile(r'["\';:<>&|?=%^\\*(),./\[\]{}]')
def _clean_label(t: str) -> str:
t = t.strip().strip('"\'')
t = _UNSAFE_RE.sub('', t).strip()
t = re.sub(r'\s{2,}', ' ', t)
return t[:40] or "step" # cap label length; never empty
# ββ Reserved keywords that must never be node IDs βββββββββββββββ #
_RESERVED = frozenset({
'flowchart', 'graph', 'subgraph', 'end', 'style', 'classDef',
'class', 'click', 'call', 'href', 'linkStyle', 'direction',
'TB', 'TD', 'BT', 'RL', 'LR',
})
# ββ Patterns ββββββββββββββββββββββββββββββββββββββββββββββββββββ #
# Matches any node definition: A["label"], A[label], A("label"), A{label}
NODE_DEF_RE = re.compile(
r'\b([A-Za-z][A-Za-z0-9_]*)\s*'
r'([\[{\(])'
r'((?:"[^"]*"|\'[^\']*\'|[^\]}\)])*)'
r'[\]}\)]'
)
# Matches edges: A --> B or A -->|label| B or A -- label --> B
EDGE_RE = re.compile(
r'\b([A-Za-z][A-Za-z0-9_]*)\b\s*'
r'(?:--+>|==+>|-\.+->)\s*'
r'(?:\|[^|\n]*\|)?\s*' # discard any edge label
r'\b([A-Za-z][A-Za-z0-9_]*)\b'
)
EDGE_LABEL_MID_RE = re.compile( # A -- label --> B
r'\b([A-Za-z][A-Za-z0-9_]*)\b\s*--\s*[^->\n]+?\s*-->\s*'
r'\b([A-Za-z][A-Za-z0-9_]*)\b'
)
# ββ Parse βββββββββββββββββββββββββββββββββββββββββββββββββββββββ #
nodes: dict[str, str] = {} # id β label
edges: list[tuple[str, str]] = [] # (src, dst) β no labels kept
_in_subgraph = False
for line in lines:
stripped = line.strip()
if not stripped or stripped.startswith('%'):
continue
lower = stripped.lower()
if lower.startswith(('flowchart', 'graph')):
continue
if lower.startswith('subgraph'):
_in_subgraph = True
continue
if lower == 'end' and _in_subgraph:
_in_subgraph = False
continue
if _in_subgraph:
continue
# Collect node definitions
for nm in NODE_DEF_RE.finditer(stripped):
nid, content = nm.group(1), nm.group(3)
if nid in _RESERVED:
continue
if nid not in nodes:
nodes[nid] = _clean_label(content)
# Strip node definitions before edge matching
line_for_edges = NODE_DEF_RE.sub(lambda m: m.group(1), stripped)
# Try A -- label --> B first, then plain A --> B
m = EDGE_LABEL_MID_RE.search(line_for_edges)
if m:
edges.append((m.group(1), m.group(2)))
else:
m = EDGE_RE.search(line_for_edges)
if m:
edges.append((m.group(1), m.group(2)))
# ββ Remap reserved-keyword node IDs βββββββββββββββββββββββββββββ #
_id_remap: dict[str, str] = {}
_suffix = 0
def _safe_id(nid: str) -> str:
nonlocal _suffix
if nid not in _RESERVED:
return nid
if nid not in _id_remap:
_suffix += 1
_id_remap[nid] = f"N{_suffix}"
return _id_remap[nid]
nodes = {_safe_id(k): v for k, v in nodes.items()}
edges = [(_safe_id(s), _safe_id(d)) for s, d in edges]
# Ensure every node referenced in an edge has a definition
for src, dst in edges:
for nid in (src, dst):
if nid not in nodes:
nodes[nid] = nid
if not nodes and not edges:
logger.warning("Mermaid parser: nothing extracted β plain-text fallback")
return "_Flowchart could not be rendered β the diagram syntax was invalid._"
# ββ Deduplicate edges βββββββββββββββββββββββββββββββββββββββββββ #
seen: set[tuple[str, str]] = set()
unique_edges: list[tuple[str, str]] = []
for s, d in edges:
if (s, d) not in seen:
seen.add((s, d))
unique_edges.append((s, d))
edges = unique_edges
# ββ Remove back-edges (DFS cycle detection) βββββββββββββββββββββ #
adj: dict[str, list[str]] = {}
for s, d in edges:
adj.setdefault(s, []).append(d)
WHITE, GRAY, BLACK = 0, 1, 2
color: dict[str, int] = {n: WHITE for n in nodes}
back_pairs: set[tuple[str, str]] = set()
for start in list(nodes):
if color.get(start, WHITE) != WHITE:
continue
stack = [(start, iter(adj.get(start, [])))]
color[start] = GRAY
while stack:
node, children = stack[-1]
try:
child = next(children)
if color.get(child, WHITE) == GRAY:
back_pairs.add((node, child))
elif color.get(child, WHITE) == WHITE:
color[child] = GRAY
stack.append((child, iter(adj.get(child, []))))
except StopIteration:
color[node] = BLACK
stack.pop()
edges = [(s, d) for s, d in edges if (s, d) not in back_pairs]
# ββ Drop orphaned subgraphs β keep only nodes reachable from root β #
# The LLM sometimes generates disconnected subgraphs (e.g. nodes D and E
# with DβE but no edge from the main flow into D). Find the root (first
# node with no incoming edges) then BFS to collect only reachable nodes.
_incoming: set[str] = {d for _, d in edges}
_root_candidates = [n for n in nodes if n not in _incoming]
if _root_candidates:
_root = _root_candidates[0]
_adj: dict[str, list[str]] = {}
for s, d in edges:
_adj.setdefault(s, []).append(d)
_reachable: set[str] = set()
_queue = [_root]
while _queue:
_cur = _queue.pop(0)
if _cur in _reachable:
continue
_reachable.add(_cur)
for _nb in _adj.get(_cur, []):
if _nb not in _reachable:
_queue.append(_nb)
_dropped = set(nodes) - _reachable
if _dropped:
logger.info("Mermaid: dropping orphaned nodes %s", _dropped)
nodes = {k: v for k, v in nodes.items() if k in _reachable}
edges = [(s, d) for s, d in edges if s in _reachable and d in _reachable]
# ββ Cap at 10 nodes βββββββββββββββββββββββββββββββββββββββββββββ #
MAX_NODES = 10
if len(nodes) > MAX_NODES:
keep = set(list(nodes)[:MAX_NODES])
nodes = {k: v for k, v in nodes.items() if k in keep}
edges = [(s, d) for s, d in edges if s in keep and d in keep]
# ββ Sanity check βββββββββββββββββββββββββββββββββββββββββββββββ #
if len(nodes) < 2 or not edges:
logger.warning("Mermaid rebuild: too little structure (%d nodes, %d edges)", len(nodes), len(edges))
return "_Flowchart could not be rendered β not enough structure was extracted._"
# ββ Rebuild: rectangles only, plain arrows, no edge labels ββββββ #
out_lines = ["graph TD"]
for nid, label in nodes.items():
out_lines.append(f' {nid}["{label}"]')
for src, dst in edges:
out_lines.append(f' {src} --> {dst}')
result = "\n".join(out_lines)
logger.info("Mermaid rebuilt (%d nodes, %d edges):\n%s", len(nodes), len(edges), result)
return f"```mermaid\n{result}\n```"
# ------------------------------------------------------------------ #
# Core run handler #
# ------------------------------------------------------------------ #
def run_agent(
task: str,
language: str,
model: str,
session_history_state,
skip_clarification: bool = False,
):
"""
Main handler β runs the full pipeline and yields UI updates.
Generator function so Gradio can cancel it via the Stop button.
Uses LangGraph streaming to yield per-node pipeline visualiser
updates, giving live node-by-node animation.
Yields:
1. Immediately β clears outputs and shows "Running..." in status bar.
2. After each LangGraph node β updates pipeline_vis and status_bar.
3. After pipeline completes β populates all output components.
"""
# Gradio serializes gr.State to JSON between requests β reconstruct
# SessionHistory if it was deserialised as a plain list or dict.
if not isinstance(session_history_state, SessionHistory):
session_history_state = SessionHistory()
_empty = ("", "", "", "", gr.update(visible=False), "", "", "")
_no_change = (
gr.update(), gr.update(), gr.update(), gr.update(), gr.update(),
gr.update(), gr.update(), gr.update(),
)
# 2 trailing no-ops for clarification panel (hidden by default)
_clarify_hidden = (gr.update(visible=False), gr.update())
# 4 trailing no-ops for HITL components (hidden by default)
_hitl_hidden = (gr.update(visible=False), gr.update(), gr.update(), gr.update())
if not task or not task.strip():
yield _empty + ("Please enter a task description.", session_history_state, get_pipeline_html(), gr.update(), gr.update(visible=False, value=""), gr.update(), gr.update()) + _clarify_hidden + _hitl_hidden
return
# "auto" means ModelRouter decides; anything else is a pinned override
if model and model != "auto":
settings.groq_model_primary = model
start_time = time.time()
# ββ Reset error cache for this run ββββββββββββββββββββββββββββββ #
from agents.debug_agent import _error_cache as _debug_cache
_debug_cache.reset()
# ββ Step 1: Non-coding task gate (runs silently, no card animation) #
# Reject non-programming tasks immediately before any other checks.
from agents.detect_agent import DetectAgent
_detector = DetectAgent()
is_coding, gate_reason = _detector.is_coding_task(task)
if not is_coding:
session_history_state.record_skip(task, gate_reason)
_skip_msg = (
"β οΈ **I'm a code assistant for Python and SQL.**\n\n"
"I can't help with questions like this. Please give me a programming task β for example:\n"
"- *Write a Python function to sort a list*\n"
"- *Write a SQL query to find the top 5 customers by revenue*"
)
yield _empty + (
_skip_msg,
session_history_state,
get_pipeline_html(),
gr.update(),
gr.update(visible=False, value=""),
gr.update(),
gr.update(),
) + _clarify_hidden + _hitl_hidden
return
# ββ Step 2: Scope check βββββββββββββββββββββββββββββββββββββββββββ
# Confirmed coding task β but is it a single snippet or a full project?
# "Build a YouTube app" passes the coding gate but is weeks of work.
_in_scope, _scope_suggestion = _detector.is_in_scope(task)
if not _in_scope:
session_history_state.record_skip(task, "out of scope β full project, not a single snippet")
_suggestion_line = (
f"\n\nFor example: *{_scope_suggestion}*" if _scope_suggestion else ""
)
_scope_msg = (
"π¦ **That's a large project, not a single code snippet.**\n\n"
"This assistant generates focused Python/SQL code β a single function, "
"query, or short script (β€ ~100 lines).\n\n"
"Could you break it down into a specific, concrete task?"
f"{_suggestion_line}"
)
yield _empty + (
_scope_msg,
session_history_state,
get_pipeline_html(),
gr.update(),
gr.update(visible=False, value=""),
gr.update(),
gr.update(),
) + _clarify_hidden + _hitl_hidden
return
# ββ Step 3: Clarification check ββββββββββββββββββββββββββββββββββ
# In-scope coding task β check if the requirement is specific enough.
# Skipped when the user has already answered a clarification question.
if not skip_clarification:
yield _empty + (
"β³ Checking task clarity...",
session_history_state,
get_pipeline_html_with_update("clarify", 0, False, {}),
gr.update(),
gr.update(visible=False, value=""),
gr.update(),
gr.update(),
) + _clarify_hidden + _hitl_hidden
from agents.clarification_agent import ClarificationAgent
_clarify_result = ClarificationAgent().check(task)
if not _clarify_result["clear"]:
_question = _clarify_result["question"]
logger.info("ClarificationAgent: needs clarification β %s", _question)
_clarify_visible = (
gr.update(visible=True), # clarification_panel
gr.update(value=_question), # clarification_q_md
)
yield _empty + (
"π€ Please clarify your task before I proceed.",
session_history_state,
get_pipeline_html_with_update("clarify", 0, True, {}),
gr.update(),
gr.update(visible=False, value=""),
gr.update(),
gr.update(),
) + _clarify_visible + _hitl_hidden
return
# ββ Step 3: Show Language Detector as active before detection runs #
yield _empty + ("β³ Detecting language...", session_history_state, get_pipeline_html_with_update("detect", 0, False, {}), gr.update(), gr.update(visible=False, value=""), gr.update(), gr.update()) + _clarify_hidden + _hitl_hidden
# ββ Language resolution βββββββββββββββββββββββββββββββββββββββββ #
# "auto" β run model detection; "python"/"sql" β trust user's choice
_is_auto_lang = (language == "auto")
try:
detection = _detector.detect(task)
if _is_auto_lang:
# Auto mode: use detected language (fall back to Python)
if detection.language != Language.UNKNOWN and detection.confidence in ("high", "medium"):
lang = detection.language
logger.info(
"run_agent: auto-detected language=%s (%s)",
lang.value, detection.confidence,
)
else:
lang = Language.PYTHON
logger.info("run_agent: detection low-confidence, defaulting to Python")
else:
# Manual selection: honour the radio as-is
lang = Language(language.lower()) if language.lower() in ("python", "sql") else Language.PYTHON
except Exception as e:
logger.warning("run_agent: detection failed (%s), using fallback", e)
if _is_auto_lang:
lang = Language.PYTHON
else:
lang = Language(language.lower()) if language.lower() in ("python", "sql") else Language.PYTHON
# Build detection badge message
_lang_cap = lang.value.capitalize()
if _is_auto_lang:
_detect_msg = f'<span style="color:#22C55E;font-size:12px;">β Detected {_lang_cap}. Using {_lang_cap} runtime.</span>'
else:
_detect_msg = f'<span style="color:#22C55E;font-size:12px;">β Selected {_lang_cap}. Using {_lang_cap} runtime.</span>'
_detect_badge_update = gr.update(visible=True, value=_detect_msg)
# Yield the badge immediately after detection using _no_change so nothing else
# is touched. This guarantees the badge renders on the FIRST run β Gradio batches
# rapid pre-pipeline yields on first page load and only paints the last state, so
# putting the badge inside a yield that also clears outputs (via _empty) risks it
# never being painted on its own frame. A dedicated no-change yield gives the
# browser a clean frame just for the badge.
yield _no_change + (
"β³ Running pipeline...",
session_history_state,
gr.update(), # pipeline_vis β no change (keep detect:active)
gr.update(), # language_radio β no change
_detect_badge_update, # detect_badge β show NOW
gr.update(), # test_summary_md
gr.update(), # download_btn
) + _clarify_hidden + _hitl_hidden
from agents.model_router import route_models
# Pre-run router so the plan node skips it and model badges appear from step 1
prebuilt_ma = route_models(task, lang.value, model).to_dict()
initial_state = PipelineState(task=task, language=lang, model_assignments=prebuilt_ma)
graph = build_graph()
logger.info(
"run_agent: starting pipeline β task: %s, language: %s",
task[:60], lang.value,
)
last_chunk: dict = {}
try:
for chunk in graph.stream(initial_state, stream_mode="values", config={"recursion_limit": settings.graph_recursion_limit}):
last_chunk = chunk
# Skip the initial state snapshot (status == idle)
raw_status = chunk.get("status", PipelineStatus.IDLE)
if raw_status == PipelineStatus.IDLE:
continue
status_val = raw_status.value if hasattr(raw_status, "value") else str(raw_status)
iterations = chunk.get("debug_iterations", 0) or 0
is_error = raw_status in (
PipelineStatus.CLASSIFYING,
PipelineStatus.DEBUGGING,
PipelineStatus.AWAITING_HUMAN,
PipelineStatus.TEST_FAILING,
PipelineStatus.TEST_DEBUGGING,
)
# Map status β LangGraph node name for the visualiser
_status_to_node = {
"clarifying": "clarify",
"planning": "plan",
"generating": "generate",
"regenerating": "regen", # fresh regen after debug exhausted
"executing": "execute",
"classifying": "classify_error",
"debugging": "debug_agent",
"testing": "test",
"test_failing": "test",
"test_debugging": "test_debug_agent",
"explaining": "explain",
"success": "success",
"awaiting_human": "human_loop",
}
node_name = _status_to_node.get(status_val, "")
ma = chunk.get("model_assignments") or {}
pipeline_html = (
get_pipeline_html_with_update(node_name, iterations, is_error, ma)
if node_name
else get_pipeline_html()
)
yield _no_change + (
f"β³ {status_val.replace('_', ' ').capitalize()}...",
session_history_state,
pipeline_html,
gr.update(),
gr.update(), # detect_badge β no change during streaming
gr.update(),
gr.update(),
) + _clarify_hidden + _hitl_hidden
except Exception as e:
logger.error("run_agent: pipeline error: %s", e)
yield _empty + (f"β Pipeline error: {e}", session_history_state, get_pipeline_html(), gr.update(), gr.update(), gr.update(), gr.update()) + _clarify_hidden + _hitl_hidden
return
if not last_chunk:
yield _empty + ("β Pipeline returned no results.", session_history_state, get_pipeline_html(), gr.update(), gr.update(), gr.update(), gr.update()) + _clarify_hidden + _hitl_hidden
return
elapsed = round(time.time() - start_time, 2)
# ββ Build final PipelineState from last streaming chunk βββββββββ #
try:
final_state = PipelineState(**last_chunk)
except Exception as e:
logger.error("run_agent: could not reconstruct final state: %s", e)
yield _empty + (f"β State error: {e}", session_history_state, get_pipeline_html(), gr.update(), gr.update(), gr.update(), gr.update()) + _clarify_hidden + _hitl_hidden
return
session_history_state.record(final_state, exec_time=elapsed)
# ββ Extract outputs βββββββββββββββββββββββββββββββββββββββββββββ #
final_code = final_state.final_code()
tests = final_state.generated_tests
explanation = final_state.explanation
flowchart = _render_mermaid(final_state.flowchart_mermaid)
status = final_state.status.value
iterations = final_state.debug_iterations
tokens = final_state.token_usage.total_tokens
history_md = session_history_state.to_markdown()
# ββ Reflection summary ββββββββββββββββββββββββββββββββββββββββββ #
reflection_summary = ""
if final_state.reflections_history:
last_r = final_state.reflections_history[-1]
reflection_summary = (
f"**Last reflection (iteration {iterations}):**\n\n"
f"- Saw: {last_r.what_i_saw}\n"
f"- Thought: {last_r.what_i_think}\n"
f"- Did: {last_r.what_i_will_do}"
)
_status_colored = (
f'<span style="color:#22C55E;">{status}</span>'
if status == "success"
else f'<span style="color:#F59E0B;">partial success</span>'
if status == "partial_success"
else f'<span style="color:#EF4444;">{status}</span>'
if status in ("failed", "error")
else status
)
_sep = '<span style="color:#4B5563;"> | </span>'
stats_summary = (
f'<span style="color:#60A5FA;font-weight:600;">Status:</span> '
f'<span style="color:#60A5FA;">{_status_colored}</span>{_sep}'
f'<span style="color:#A78BFA;font-weight:600;">Iterations:</span> '
f'<span style="color:#A78BFA;">{iterations}</span>{_sep}'
f'<span style="color:#FBBF24;font-weight:600;">Tokens:</span> '
f'<span style="color:#FBBF24;">{tokens:,}</span>{_sep}'
f'<span style="color:#34D399;font-weight:600;">Time:</span> '
f'<span style="color:#34D399;">{elapsed:.1f}s</span>'
)
# Final pipeline HTML
is_hitl = final_state.status == PipelineStatus.AWAITING_HUMAN
is_partial = final_state.status == PipelineStatus.PARTIAL_SUCCESS
final_node_name = "human_loop" if is_hitl else ("partial_success" if is_partial else "success")
final_pipeline_html = get_pipeline_html_with_update(
final_node_name, iterations, is_hitl or is_partial,
final_state.model_assignments or {},
)
# ββ Test summary and downloadable report βββββββββββββββββββββββ #
test_summary = _build_test_summary(final_state.test_result, final_state.generated_tests or "")
report_md = _generate_report(task, lang.value, final_state, elapsed)
import tempfile, os
report_path = os.path.join(tempfile.gettempdir(), "autodev_report.md")
with open(report_path, "w", encoding="utf-8") as f:
f.write(report_md)
logger.info(
"run_agent: done β status=%s tokens=%d time=%.1fs",
status, tokens, elapsed,
)
# ββ HITL panel updates ββββββββββββββββββββββββββββββββββββββββββ #
if is_hitl:
last_error = ""
if final_state.execution_result and final_state.execution_result.error_msg:
last_error = final_state.execution_result.error_msg
hitl_updates = (
gr.update(visible=True),
gr.update(value=last_error),
gr.update(choices=final_state.hitl_options or [], value=None),
gr.update(value=final_code or ""),
)
status_msg = '<span style="color:#EF4444;">β Max retries reached β edit the code below and click Resubmit, or choose a fix strategy.</span>'
elif is_partial:
hitl_updates = _hitl_hidden
status_msg = (
'<span style="color:#F59E0B;">β Pipeline complete β code runs, but not all tests passed '
'after all retry cycles. Check the Tests tab for details.</span>'
)
else:
hitl_updates = _hitl_hidden
status_msg = f'<span style="color:#22C55E;">β Pipeline complete β {status}</span>'
yield (
final_code, tests, explanation, flowchart, gr.update(visible=bool(flowchart)),
reflection_summary, stats_summary, history_md,
status_msg,
session_history_state,
final_pipeline_html,
gr.update(), # language_radio β keep user's selection unchanged
_detect_badge_update, # detect_badge β show "β Detected/Selected X..."
gr.update(visible=bool(test_summary), value=test_summary),
gr.update(visible=not is_hitl, value=report_path if not is_hitl else None),
) + _clarify_hidden + hitl_updates
def submit_clarification(
answer: str,
original_task: str,
language: str,
model: str,
session_history_state,
):
"""
Re-run the pipeline after the user provides clarification.
Appends the clarification to the original task and re-runs.
Skips the clarification check on re-run β user has already answered once.
"""
if not answer or not answer.strip():
yield from run_agent(original_task, language, model, session_history_state, skip_clarification=True)
return
refined_task = f"{original_task.strip()}\n\nAdditional context: {answer.strip()}"
yield from run_agent(refined_task, language, model, session_history_state, skip_clarification=True)
def resubmit_agent(
edited_code: str,
chosen_option: str,
original_task: str,
language: str,
model: str,
session_history_state,
):
"""
Re-run the pipeline after the user edits code or selects a fix strategy
from the HITL panel.
Builds a revised task that includes the user's edited code (or chosen
fix hint) so the agent has extra context on its next attempt.
"""
hint = ""
if chosen_option:
hint = f"\n\nApply this specific fix strategy: {chosen_option}"
if edited_code and edited_code.strip():
hint += f"\n\nHere is my edited version of the code β continue from here:\n```\n{edited_code.strip()}\n```"
revised_task = original_task.strip() + hint
yield from run_agent(revised_task, language, model, session_history_state)
def detect_language(task: str) -> tuple:
"""
Run auto-detection and return badge text + radio pre-selection.
Args:
task: The user's task description.
Returns:
Tuple of (badge_markdown, detected_language_string).
"""
if not task or len(task.strip()) < 5:
return "", "python"
try:
agent = DetectAgent()
result = agent.detect(task)
lang = result.language.value if result.language != Language.UNKNOWN else "python"
emoji = {"high": "π’", "medium": "π‘", "low": "π΄"}.get(result.confidence, "βͺ")
badge = (
f"{emoji} Auto-detected: **{lang}** "
f"({result.confidence} confidence) β {result.reason}"
)
return badge, lang
except Exception as e:
logger.warning("detect_language failed: %s", e)
return "", "python"
def run_benchmark(
model: str,
progress: gr.Progress = gr.Progress(),
):
"""
Run the full evaluation harness, yielding status bar updates and
the final Markdown report.
Yields:
Tuples of (benchmark_output, status_bar).
"""
settings.groq_model_primary = model
runner = BenchmarkRunner()
total = len(runner.tasks)
results = []
yield "", "β³ Starting benchmark..."
for i, task in enumerate(runner.tasks):
progress((i / total), desc=f"Running: {task.name}...")
yield "", f"β³ Running task {i + 1}/{total}: {task.name}..."
result = runner._run_task(task)
status_icon = "β
" if result.success else "β"
results.append(result)
yield "", f"{status_icon} Task {i + 1}/{total} done: {task.name}"
progress(1.0, desc="Benchmark complete.")
summary = compute_metrics(results)
tracker = WandbTracker()
tracker.log_benchmark_run(results, summary)
tracker.finish()
pct = round(summary.success_rate * 100)
report = _format_benchmark_report(results, summary, model)
yield report, f"β Benchmark complete β {summary.successful_tasks}/{summary.total_tasks} passed ({pct}%)"
def _format_benchmark_report(results: list, summary, model: str) -> str:
"""
Build the full Markdown benchmark report shown in the UI.
Sections:
- Header (date, model, max retries)
- Task Results table (pipe-aligned)
- Summary Metrics
- Failed Task Details (one block per failed task)
"""
import datetime
from config import settings as cfg
date_str = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# ββ Header ββββββββββββββββββββββββββββββββββββββββββββββββββββββ #
lines = [
"# AutoDevAgent β Benchmark Report",
f"**Date:** {date_str} ",
f"**Model:** {model} ",
f"**Max retries:** {cfg.max_debug_retries}",
"",
"## Task Results",
"",
]
# ββ Build table rows ββββββββββββββββββββββββββββββββββββββββββββ #
headers = ["ID", "Task", "Language", "Success", "Iterations", "Tokens", "Time (s)"]
rows = []
for i, r in enumerate(results, 1):
iters = f"{r.iterations} (max)" if not r.success and r.iterations >= cfg.max_debug_retries else str(r.iterations)
rows.append([
str(i),
r.task_name,
r.language.capitalize(),
"β
" if r.success else "β",
iters,
f"{r.total_tokens:,}",
f"{r.exec_time:.1f}",
])
# Compute column widths (max of header and each cell)
col_widths = [len(h) for h in headers]
for row in rows:
for j, cell in enumerate(row):
col_widths[j] = max(col_widths[j], len(cell))
def fmt_row(cells):
return "| " + " | ".join(c.ljust(col_widths[j]) for j, c in enumerate(cells)) + " |"
def sep_row():
return "|-" + "-|-".join("-" * w for w in col_widths) + "-|"
lines.append(fmt_row(headers))
lines.append(sep_row())
for row in rows:
lines.append(fmt_row(row))
# ββ Summary Metrics βββββββββββββββββββββββββββββββββββββββββββββ #
pct = round(summary.success_rate * 100)
total_time = round(sum(r.exec_time for r in results), 1)
lines += [
"",
"## Summary Metrics",
"",
f"- **Success rate:** {pct}% ({summary.successful_tasks}/{summary.total_tasks})",
f"- **Average debug iterations:** {summary.avg_iterations:.1f}",
f"- **Average tokens per task:** {summary.avg_tokens:,.0f}",
f"- **Total execution time:** {total_time} seconds",
]
# ββ Failed Task Details βββββββββββββββββββββββββββββββββββββββββ #
failed = [r for r in results if not r.success]
if failed:
lines += ["", "## Failed Task Details", ""]
for i, r in enumerate(results, 1):
if r.success:
continue
last_error = r.error_message or "Unknown error"
lines += [
f"### Task {i}: {r.task_name}",
f"- **Error:** Max retries exceeded ({r.iterations})",
f"- **Last error:** {last_error}",
"",
]
return "\n".join(lines)
# ------------------------------------------------------------------ #
# Gradio UI #
# ------------------------------------------------------------------ #
_fc_js = """
function fcZoom(z) {
window._fcZ = z;
var t = document.querySelector('.mermaid svg') || document.querySelector('.mermaid');
if (t) { t.style.transform = 'scale('+z+')'; t.style.transformOrigin = 'top center'; t.style.display = 'block'; }
var l = document.getElementById('fc-zl');
if (l) l.textContent = Math.round(z * 100) + '%';
}
window._fcZ = 1;
"""
def build_ui() -> gr.Blocks:
"""
Assemble the full Gradio Blocks UI.
Returns:
Configured gr.Blocks instance ready to launch.
"""
with gr.Blocks(title="AutoDevAgent") as demo:
# ββ Session state βββββββββββββββββββββββββββββββββββββββββββ #
session_state = gr.State(SessionHistory())
# ββ Header βββββββββββββββββββββββββββββββββββββββββββββββββ #
gr.Markdown(
"# AutoDevAgent\n"
"Autonomous code generation, execution, debugging, and testing "
"β powered by LangChain + LangGraph + Groq (Llama 3.1) Β· "
"[GitHub](https://github.com) Β· "
"[HuggingFace Space](https://huggingface.co/spaces/SivaSai8143/ai-research-digest)"
)
with gr.Row():
# ββ Left column βββββββββββββββββββββββββββββββββββββββββ #
with gr.Column(scale=1, elem_id="left_col"):
# ββ Top-left: task inputs βββββββββββββββββββββββββββ #
task_input = gr.Textbox(
label = "Task description",
placeholder = "Write your task here or select an example below",
lines = 4,
elem_id = "task_input",
)
with gr.Row(elem_id="lang_row"):
with gr.Column(scale=0, min_width=80):
gr.Markdown("**Language**")
language_radio = gr.Radio(
choices = [("Python", "python"), ("SQL", "sql"), ("Auto (recommended)", "auto")],
value = "auto",
label = None,
container = False,
elem_id = "language_radio",
scale = 1,
)
detect_badge = gr.Markdown(
value = "",
visible = False,
elem_id = "detect_badge",
)
with gr.Row(elem_id="model_row"):
with gr.Column(scale=0, min_width=80):
gr.Markdown("**Model**")
model_radio = gr.Dropdown(
choices = [
("π€ Auto β smart routing (recommended)", "auto"),
("β‘ Llama 3.1 8B β fastest", "llama-3.1-8b-instant"),
("βοΈ Llama 4 Scout 17B β balanced", "meta-llama/llama-4-scout-17b-16e-instruct"),
("πͺ Llama 3.3 70B β most capable", "llama-3.3-70b-versatile"),
],
value = "auto",
label = None,
container = False,
elem_id = "model_dropdown",
scale = 1,
)
_all_examples = (
["Select an example task βΌ"]
+ ["ββ Python ββ"]
+ PYTHON_EXAMPLES
+ ["ββ SQL ββ"]
+ SQL_EXAMPLES
)
with gr.Row(elem_id="examples_row"):
with gr.Column(scale=0, min_width=80):
gr.Markdown("**Examples**")
example_dropdown = gr.Dropdown(
choices = _all_examples,
value = "Select an example task βΌ",
label = None,
container = False,
elem_id = "example_dropdown",
scale = 1,
)
with gr.Row():
run_btn = gr.Button("βΆ Run", variant="primary", size="sm")
stop_btn = gr.Button("β Stop", variant="stop", size="sm")
clear_btn = gr.Button("β Clear", variant="secondary", size="sm")
# ββ Bottom-left: output tabs ββββββββββββββββββββββββ #
gr.Markdown("---")
# ββ Status bar ββββββββββββββββββββββββββββββββββββββ #
status_bar = gr.Markdown(value="Ready.", elem_id="status_bar")
with gr.Tabs(elem_id="output_tabs"):
with gr.TabItem("Code"):
code_output = gr.Code(
label = "Generated code",
language = "python",
interactive = False,
max_lines = 30,
elem_id = "code_output",
)
with gr.TabItem("Tests"):
test_summary_md = gr.Markdown(value="", visible=False, elem_id="test_summary_md")
test_output = gr.Code(
label = "Generated tests",
language = "python",
interactive = False,
max_lines = 30,
elem_id = "test_output",
)
download_btn = gr.DownloadButton(
label = "Download Report",
visible = False,
elem_id = "download_btn",
)
with gr.TabItem("Explanation"):
explain_output = gr.Markdown(value="_No explanation generated yet._", label="Plain-English explanation", elem_id="explain_output")
with gr.TabItem("Flowchart"):
fc_zoom_bar = gr.HTML(visible=False, value="""
<div style="display:flex;gap:8px;padding:6px 2px 10px;align-items:center;">
<button onclick="fcZoom(Math.min(4,(window._fcZ||1)+0.25))" style="padding:2px 13px;cursor:pointer;border-radius:4px;border:1px solid #888;font-size:15px;background:transparent;color:inherit">+</button>
<button onclick="fcZoom(Math.max(0.25,(window._fcZ||1)-0.25))" style="padding:2px 13px;cursor:pointer;border-radius:4px;border:1px solid #888;font-size:15px;background:transparent;color:inherit">β</button>
<button onclick="fcZoom(1)" style="padding:2px 10px;cursor:pointer;border-radius:4px;border:1px solid #888;font-size:12px;background:transparent;color:inherit">Reset</button>
<span id="fc-zl" style="font-size:12px;color:#888;margin-left:2px">100%</span>
</div>
""")
flowchart_output = gr.Markdown(
value = "_No flowchart generated yet._",
label = "Logic flowchart",
elem_id = "flowchart_output",
)
# ββ Stats + reflection ββββββββββββββββββββββββββββββ #
stats_md = gr.Markdown(value="", elem_id="stats_md")
with gr.Accordion("Self-reflection log", open=False, visible=False) as reflection_panel:
reflection_md = gr.Markdown(value="")
# ββ Clarification panel βββββββββββββββββββββββββββββ #
clarification_components = build_clarification_panel()
# ββ HITL panel ββββββββββββββββββββββββββββββββββββββ #
hitl_components = build_hitl_panel()
# ββ Session history βββββββββββββββββββββββββββββββββ #
with gr.Accordion("Session history", open=False):
history_md = gr.Markdown(value="*No runs yet.*")
# ββ Benchmark βββββββββββββββββββββββββββββββββββββββ #
with gr.Accordion("Evaluation harness", open=False):
gr.Markdown(
"Run the 5 built-in benchmark tasks and log results to W&B. "
"Uses the currently selected model."
)
benchmark_btn = gr.Button("βΆ Run benchmark", variant="secondary")
benchmark_output = gr.Markdown(value="")
# ββ Right column: full-height pipeline visualiser βββββββ #
with gr.Column(scale=1, elem_id="pipeline_col"):
pipeline_vis = gr.HTML(
value = get_pipeline_html(),
label = "Pipeline status",
elem_id = "pipeline_vis",
)
# ββ Event handlers ββββββββββββββββββββββββββββββββββββββββββ #
# Example dropdown β fill task input, skip separator items
def _pick_example(val):
if val and not val.startswith("ββ") and not val.startswith("Select an example"):
return val, gr.update() # fill task, keep dropdown
return gr.update(), gr.update(value="Select an example task βΌ") # ignore, reset to placeholder
example_dropdown.change(
fn = _pick_example,
inputs = [example_dropdown],
outputs = [task_input, example_dropdown],
)
# Unpack HITL components for wiring
hitl_panel = hitl_components["panel"]
hitl_error_disp = hitl_components["error_display"]
hitl_options = hitl_components["options_radio"]
hitl_editor = hitl_components["code_editor"]
hitl_apply_btn = hitl_components["apply_btn"]
hitl_resub_btn = hitl_components["resubmit_btn"]
clarification_panel = clarification_components["panel"]
clarification_q_md = clarification_components["question_md"]
clarification_input = clarification_components["answer_input"]
clarification_submit = clarification_components["submit_btn"]
# Main run button β 21 outputs (15 normal + 2 clarification + 4 HITL)
run_outputs = [
code_output, test_output, explain_output,
flowchart_output, fc_zoom_bar,
reflection_md, stats_md, history_md,
status_bar,
session_state,
pipeline_vis,
language_radio,
detect_badge,
test_summary_md,
download_btn,
# Clarification panel (shown when task is unclear)
clarification_panel, clarification_q_md,
# HITL panel components (shown only on AWAITING_HUMAN)
hitl_panel, hitl_error_disp, hitl_options, hitl_editor,
]
run_event = run_btn.click(
fn = run_agent,
inputs = [task_input, language_radio, model_radio, session_state],
outputs = run_outputs,
queue = True,
)
# HITL resubmit β uses edited code + chosen option to re-run pipeline
hitl_events = []
for hitl_btn in (hitl_resub_btn, hitl_apply_btn):
evt = hitl_btn.click(
fn = resubmit_agent,
inputs = [hitl_editor, hitl_options, task_input, language_radio, model_radio, session_state],
outputs = run_outputs,
queue = True,
)
hitl_events.append(evt)
# Clarification submit β re-runs pipeline with refined task
clarify_event = clarification_submit.click(
fn = submit_clarification,
inputs = [clarification_input, task_input, language_radio, model_radio, session_state],
outputs = run_outputs,
queue = True,
)
stop_btn.click(
fn = lambda: (get_pipeline_html(), "Stopped."),
inputs = [],
outputs = [pipeline_vis, status_bar],
cancels = [run_event, clarify_event] + hitl_events,
)
# Clear button β resets all outputs and state to initial values
clear_outputs = [
task_input, code_output, test_output, explain_output,
flowchart_output, fc_zoom_bar,
reflection_md, stats_md, history_md,
status_bar, session_state, pipeline_vis,
detect_badge,
test_summary_md, download_btn,
example_dropdown,
clarification_panel, clarification_q_md,
hitl_panel, hitl_error_disp, hitl_options, hitl_editor,
]
clear_btn.click(
fn = lambda: (
"", # task_input
"", # code_output
"", # test_output
"_No explanation generated yet._", # explain_output
"_No flowchart generated yet._", # flowchart_output
gr.update(visible=False), # fc_zoom_bar
"", # reflection_md
"", # stats_md
"*No runs yet.*", # history_md
"Ready.", # status_bar
[], # session_state
get_pipeline_html(), # pipeline_vis (reset to idle)
gr.update(visible=False, value=""), # detect_badge
gr.update(visible=False, value=""), # test_summary_md
gr.update(visible=False, value=None), # download_btn
gr.update(value="Select an example task βΌ"), # example_dropdown
gr.update(visible=False), # clarification_panel
gr.update(value=""), # clarification_q_md
gr.update(visible=False), # hitl_panel
gr.update(value=""), # hitl_error_disp
gr.update(value=None), # hitl_options
gr.update(value=""), # hitl_editor
),
inputs = [],
outputs = clear_outputs,
)
# Benchmark
benchmark_btn.click(
fn = run_benchmark,
inputs = [model_radio],
outputs = [benchmark_output, status_bar],
)
return demo
# ------------------------------------------------------------------ #
# Entry point #
# ------------------------------------------------------------------ #
if __name__ == "__main__":
demo = build_ui()
demo.launch(
server_name = "0.0.0.0",
server_port = 7860,
share = False,
js = _fc_js,
theme = gr.themes.Soft(),
css = """
body, .gradio-container, .main, .app, svelte-scoped, #root {
max-width: 100% !important;
width: 100% !important;
padding-left: 0 !important;
padding-right: 0 !important;
margin-left: 0 !important;
margin-right: 0 !important;
}
.gradio-container { padding: 12px !important; box-sizing: border-box !important; }
footer { display: none !important; }
/* ββ Mermaid v11 dark theme ββ */
/* SVG canvas β constrain width so TD layout renders vertically */
.mermaid {
background: #0d1117 !important;
border-radius: 10px !important;
display: flex !important;
justify-content: center !important;
overflow-x: auto !important;
}
.mermaid svg {
background: #0d1117 !important;
border-radius: 10px !important;
width: 300px !important;
max-width: 300px !important;
height: auto !important;
display: block !important;
}
/* All node shapes: rect (process), polygon (diamond), circle, path */
.mermaid .node rect,
.mermaid .node circle,
.mermaid .node ellipse,
.mermaid .node polygon,
.mermaid .node path,
.mermaid .node .basic,
.mermaid .node .label-container,
.mermaid .cluster rect {
fill: #1e3a5f !important;
stroke: #4a90d9 !important;
stroke-width: 1.5px !important;
}
/* v11 uses foreignObject > div > .nodeLabel for label text */
.mermaid .nodeLabel,
.mermaid .nodeLabel p,
.mermaid .node .label,
.mermaid foreignObject div {
color: #e2e8f0 !important;
fill: #e2e8f0 !important;
font-family: system-ui, sans-serif !important;
font-size: 13px !important;
}
/* SVG text fallback (older sub-graphs, titles) */
.mermaid text,
.mermaid text tspan {
fill: #e2e8f0 !important;
}
/* Edge lines */
.mermaid .edgePath path,
.mermaid .flowchart-link,
.mermaid path.path {
stroke: #4a90d9 !important;
stroke-width: 2px !important;
}
/* Arrowheads */
.mermaid .arrowheadPath,
.mermaid marker path,
.mermaid .arrowMarkerPath {
fill: #4a90d9 !important;
stroke: #4a90d9 !important;
}
/* Edge labels */
.mermaid .edgeLabel,
.mermaid .edgeLabel rect,
.mermaid .edgeLabel .label {
background: #1e293b !important;
fill: #1e293b !important;
color: #94a3b8 !important;
}
.mermaid .edgeLabel span,
.mermaid .edgeLabel p {
color: #94a3b8 !important;
background: #1e293b !important;
}
/* ββ Output tabs: natural height when empty, capped when full ββ */
#output_tabs {
max-height: 350px !important;
overflow: hidden !important;
display: flex !important;
flex-direction: column !important;
}
/* Tab button bar stays fixed */
#output_tabs > .tab-nav {
flex-shrink: 0 !important;
}
/* Each tab pane scrolls when content overflows */
#output_tabs > .tabitem,
#output_tabs .tabitem > div {
overflow-y: auto !important;
max-height: 300px !important;
}
/* ββ Cap code/test editors when content fills in ββ */
#code_output .cm-editor,
#test_output .cm-editor {
max-height: 260px !important;
overflow-y: auto !important;
}
/* ββ Explanation & Flowchart markdown scroll ββ */
#explain_output,
#flowchart_output {
max-height: 200px !important;
overflow-y: auto !important;
}
/* ββ Inline label + control rows (Language / Model / Examples) ββ */
#lang_row,
#model_row,
#examples_row {
align-items: center !important;
gap: 8px !important;
margin-bottom: 4px !important;
}
#lang_row > *,
#model_row > *,
#examples_row > * {
margin-bottom: 0 !important;
}
/* Label markdown: fixed narrow width, no extra padding */
#lang_row > .prose,
#model_row > .prose,
#examples_row > .prose {
min-width: 80px !important;
max-width: 80px !important;
flex-shrink: 0 !important;
padding: 0 !important;
margin: 0 !important;
}
/* ββ Collapse test summary when empty (no gap above Generated tests) ββ */
#test_summary_md:empty,
#test_summary_md .prose:empty,
#test_summary_md > .block:empty {
display: none !important;
}
#test_summary_md {
min-height: 0 !important;
padding: 0 !important;
margin: 0 !important;
}
/* ββ Language detection badge ββ */
#detect_badge {
min-height: 0 !important;
padding: 0 !important;
margin: 2px 0 4px 0 !important;
}
#detect_badge .prose {
font-size: 12px !important;
color: #22C55E !important;
padding: 0 !important;
margin: 0 !important;
}
/* ββ Pipeline column: natural width, no forced height ββ */
#pipeline_col {
min-width: 0;
}
""",
)
|