copilot-swe-agent[bot] CatoG commited on
Commit ยท
3a88dde
1
Parent(s): 09ba91c
Add agent role checkboxes and 3 new roles to Multi-Role Workflow tab
Browse filesCo-authored-by: CatoG <47473856+CatoG@users.noreply.github.com>
app.py
CHANGED
|
@@ -507,14 +507,29 @@ TOOL_NAMES = list(ALL_TOOLS.keys())
|
|
| 507 |
|
| 508 |
MAX_REVISIONS = 3 # Maximum QA-driven revision cycles before accepting best attempt
|
| 509 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 510 |
|
| 511 |
class WorkflowState(TypedDict):
|
| 512 |
"""Shared, inspectable state object threaded through the whole workflow."""
|
| 513 |
user_request: str
|
| 514 |
plan: str
|
| 515 |
-
current_role: str # "creative" or "
|
| 516 |
creative_output: str
|
| 517 |
technical_output: str
|
|
|
|
|
|
|
|
|
|
| 518 |
draft_output: str # latest specialist output forwarded to QA
|
| 519 |
qa_report: str
|
| 520 |
qa_passed: bool
|
|
@@ -528,12 +543,16 @@ _PLANNER_SYSTEM = (
|
|
| 528 |
"You are the Planner in a multi-role AI workflow.\n"
|
| 529 |
"Your job is to:\n"
|
| 530 |
"1. Break the user's task into clear subtasks.\n"
|
| 531 |
-
"2. Decide which specialist to call:
|
| 532 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 533 |
"3. State clear success criteria.\n\n"
|
| 534 |
"Respond in this exact format:\n"
|
| 535 |
"TASK BREAKDOWN:\n<subtask list>\n\n"
|
| 536 |
-
"ROLE TO CALL: <Creative Expert | Technical Expert>\n\n"
|
| 537 |
"SUCCESS CRITERIA:\n<what a correct, complete answer looks like>\n\n"
|
| 538 |
"GUIDANCE FOR SPECIALIST:\n<any constraints or focus areas>"
|
| 539 |
)
|
|
@@ -574,10 +593,39 @@ _PLANNER_REVIEW_SYSTEM = (
|
|
| 574 |
"FINAL ANSWER:\n<the approved specialist output, reproduced in full>\n\n"
|
| 575 |
"If QA FAILED, respond with:\n"
|
| 576 |
"DECISION: REVISE\n"
|
| 577 |
-
"ROLE TO CALL: <Creative Expert | Technical Expert>\n"
|
| 578 |
"REVISED INSTRUCTIONS:\n<specific fixes the specialist must address>"
|
| 579 |
)
|
| 580 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 581 |
|
| 582 |
# --- Internal helpers ---
|
| 583 |
|
|
@@ -594,7 +642,7 @@ def _decide_role(text: str) -> str:
|
|
| 594 |
"""Parse which specialist role the Planner wants to invoke.
|
| 595 |
|
| 596 |
Checks for the expected structured 'ROLE TO CALL:' format first,
|
| 597 |
-
then falls back to a word-boundary search
|
| 598 |
Defaults to 'technical' when no clear signal is found.
|
| 599 |
"""
|
| 600 |
# Prefer the explicit structured label produced by the Planner prompt
|
|
@@ -602,10 +650,21 @@ def _decide_role(text: str) -> str:
|
|
| 602 |
return "creative"
|
| 603 |
if "ROLE TO CALL: Technical Expert" in text:
|
| 604 |
return "technical"
|
| 605 |
-
|
| 606 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 607 |
if re.search(r"\bcreative\b", text, re.IGNORECASE):
|
| 608 |
return "creative"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 609 |
return "technical"
|
| 610 |
|
| 611 |
|
|
@@ -735,6 +794,67 @@ def _step_planner_review(chat_model, state: WorkflowState, trace: List[str]) ->
|
|
| 735 |
return state
|
| 736 |
|
| 737 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 738 |
# --- Specialist role tools ---
|
| 739 |
# These wrap the step functions as @tool so the Planner (or any LangChain agent)
|
| 740 |
# can invoke specialists in a standard tool-use pattern.
|
|
@@ -742,16 +862,20 @@ def _step_planner_review(chat_model, state: WorkflowState, trace: List[str]) ->
|
|
| 742 |
# Holds the active model ID for standalone specialist tool calls.
|
| 743 |
_workflow_model_id: str = DEFAULT_MODEL_ID
|
| 744 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 745 |
|
| 746 |
@tool
|
| 747 |
def call_creative_expert(task: str) -> str:
|
| 748 |
"""Call the Creative Expert to brainstorm ideas, framing, and produce a draft for a given task."""
|
| 749 |
chat = build_provider_chat(_workflow_model_id)
|
| 750 |
-
state: WorkflowState = {
|
| 751 |
-
"user_request": task, "plan": task, "current_role": "creative",
|
| 752 |
-
"creative_output": "", "technical_output": "", "draft_output": "",
|
| 753 |
-
"qa_report": "", "qa_passed": False, "revision_count": 0, "final_answer": "",
|
| 754 |
-
}
|
| 755 |
state = _step_creative(chat, state, [])
|
| 756 |
return state["creative_output"]
|
| 757 |
|
|
@@ -760,11 +884,7 @@ def call_creative_expert(task: str) -> str:
|
|
| 760 |
def call_technical_expert(task: str) -> str:
|
| 761 |
"""Call the Technical Expert to produce implementation details and a solution for a given task."""
|
| 762 |
chat = build_provider_chat(_workflow_model_id)
|
| 763 |
-
state: WorkflowState = {
|
| 764 |
-
"user_request": task, "plan": task, "current_role": "technical",
|
| 765 |
-
"creative_output": "", "technical_output": "", "draft_output": "",
|
| 766 |
-
"qa_report": "", "qa_passed": False, "revision_count": 0, "final_answer": "",
|
| 767 |
-
}
|
| 768 |
state = _step_technical(chat, state, [])
|
| 769 |
return state["technical_output"]
|
| 770 |
|
|
@@ -782,28 +902,61 @@ def call_qa_tester(task_and_output: str) -> str:
|
|
| 782 |
task = task_and_output
|
| 783 |
output = task_and_output
|
| 784 |
# current_role is left empty โ this is a standalone QA call outside the normal loop
|
| 785 |
-
state: WorkflowState = {
|
| 786 |
-
"user_request": task, "plan": task, "current_role": "",
|
| 787 |
-
"creative_output": "", "technical_output": "", "draft_output": output,
|
| 788 |
-
"qa_report": "", "qa_passed": False, "revision_count": 0, "final_answer": "",
|
| 789 |
-
}
|
| 790 |
state = _step_qa(chat, state, [])
|
| 791 |
return state["qa_report"]
|
| 792 |
|
| 793 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 794 |
# --- Orchestration loop ---
|
| 795 |
|
| 796 |
-
def run_multi_role_workflow(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 797 |
"""Run the supervisor-style multi-role workflow.
|
| 798 |
|
| 799 |
Flow:
|
| 800 |
-
1. Planner analyses the task and picks a specialist.
|
| 801 |
-
2. Specialist
|
| 802 |
-
3. QA Tester reviews the output.
|
| 803 |
-
4. Planner reviews QA result and either approves or requests a revision.
|
| 804 |
5. Repeat from step 2 if QA fails and retries remain.
|
| 805 |
6. If max retries are reached, return best attempt with QA concerns.
|
| 806 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 807 |
Returns:
|
| 808 |
(final_answer, workflow_trace_text)
|
| 809 |
"""
|
|
@@ -811,12 +964,30 @@ def run_multi_role_workflow(message: str, model_id: str) -> Tuple[str, str]:
|
|
| 811 |
_workflow_model_id = model_id
|
| 812 |
chat_model = build_provider_chat(model_id)
|
| 813 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 814 |
state: WorkflowState = {
|
| 815 |
"user_request": message,
|
| 816 |
"plan": "",
|
| 817 |
"current_role": "",
|
| 818 |
"creative_output": "",
|
| 819 |
"technical_output": "",
|
|
|
|
|
|
|
|
|
|
| 820 |
"draft_output": "",
|
| 821 |
"qa_report": "",
|
| 822 |
"qa_passed": False,
|
|
@@ -828,44 +999,68 @@ def run_multi_role_workflow(message: str, model_id: str) -> Tuple[str, str]:
|
|
| 828 |
"โโโ MULTI-ROLE WORKFLOW STARTED โโโ",
|
| 829 |
f"Model : {model_id}",
|
| 830 |
f"Request : {message}",
|
|
|
|
| 831 |
f"Max revisions: {MAX_REVISIONS}",
|
| 832 |
]
|
| 833 |
|
| 834 |
try:
|
| 835 |
-
|
| 836 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 837 |
|
| 838 |
# Orchestration loop: specialist โ QA โ Planner review โ revise if needed
|
| 839 |
while True:
|
| 840 |
-
# Step 2: invoke the chosen specialist
|
| 841 |
-
|
| 842 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 843 |
else:
|
| 844 |
-
state =
|
| 845 |
-
|
| 846 |
-
|
| 847 |
-
|
| 848 |
-
|
| 849 |
-
|
| 850 |
-
|
| 851 |
-
|
| 852 |
-
|
| 853 |
-
|
| 854 |
-
|
| 855 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 856 |
|
| 857 |
-
|
| 858 |
-
|
| 859 |
-
|
| 860 |
state["final_answer"] = state["draft_output"]
|
| 861 |
-
trace.append(
|
| 862 |
-
f"\nโโโ MAX REVISIONS REACHED ({MAX_REVISIONS}) โโโ\n"
|
| 863 |
-
f"Returning best attempt. Outstanding QA concerns:\n{state['qa_report']}"
|
| 864 |
-
)
|
| 865 |
break
|
| 866 |
|
| 867 |
-
trace.append(f"\nโโโ REVISION {state['revision_count']} / {MAX_REVISIONS} โโโ")
|
| 868 |
-
|
| 869 |
except Exception as exc:
|
| 870 |
trace.append(f"\n[ERROR] {exc}\n{traceback.format_exc()}")
|
| 871 |
state["final_answer"] = state["draft_output"] or f"Workflow error: {exc}"
|
|
@@ -1235,9 +1430,10 @@ with gr.Blocks(title="LLM + Agent tools demo", theme=gr.themes.Soft()) as demo:
|
|
| 1235 |
with gr.Tab("Multi-Role Workflow"):
|
| 1236 |
gr.Markdown(
|
| 1237 |
"## Supervisor-style Multi-Role Workflow\n"
|
| 1238 |
-
"**Planner** โ **Specialist**
|
| 1239 |
"The Planner breaks the task, picks the right specialist, and reviews QA feedback. "
|
| 1240 |
-
f"If QA fails, the loop repeats up to **{MAX_REVISIONS}** times before accepting the best attempt."
|
|
|
|
| 1241 |
)
|
| 1242 |
|
| 1243 |
with gr.Row():
|
|
@@ -1247,15 +1443,24 @@ with gr.Blocks(title="LLM + Agent tools demo", theme=gr.themes.Soft()) as demo:
|
|
| 1247 |
label="Model",
|
| 1248 |
)
|
| 1249 |
|
| 1250 |
-
|
| 1251 |
-
|
| 1252 |
-
|
| 1253 |
-
|
| 1254 |
-
|
| 1255 |
-
|
| 1256 |
-
|
| 1257 |
-
|
| 1258 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1259 |
|
| 1260 |
with gr.Row():
|
| 1261 |
with gr.Column(scale=2):
|
|
@@ -1271,26 +1476,30 @@ with gr.Blocks(title="LLM + Agent tools demo", theme=gr.themes.Soft()) as demo:
|
|
| 1271 |
interactive=False,
|
| 1272 |
)
|
| 1273 |
|
| 1274 |
-
def _run_workflow_ui(
|
|
|
|
|
|
|
| 1275 |
"""Gradio handler: validate input, run the workflow, return outputs."""
|
| 1276 |
if not message or not message.strip():
|
| 1277 |
return "No input provided.", ""
|
| 1278 |
try:
|
| 1279 |
-
final_answer, trace = run_multi_role_workflow(
|
|
|
|
|
|
|
| 1280 |
return final_answer, trace
|
| 1281 |
except Exception as exc:
|
| 1282 |
return f"Workflow error: {exc}", traceback.format_exc()
|
| 1283 |
|
| 1284 |
wf_submit_btn.click(
|
| 1285 |
fn=_run_workflow_ui,
|
| 1286 |
-
inputs=[wf_input, wf_model_dropdown],
|
| 1287 |
outputs=[wf_answer, wf_trace],
|
| 1288 |
show_api=False,
|
| 1289 |
)
|
| 1290 |
|
| 1291 |
wf_input.submit(
|
| 1292 |
fn=_run_workflow_ui,
|
| 1293 |
-
inputs=[wf_input, wf_model_dropdown],
|
| 1294 |
outputs=[wf_answer, wf_trace],
|
| 1295 |
show_api=False,
|
| 1296 |
)
|
|
|
|
| 507 |
|
| 508 |
MAX_REVISIONS = 3 # Maximum QA-driven revision cycles before accepting best attempt
|
| 509 |
|
| 510 |
+
AGENT_ROLES = {
|
| 511 |
+
"planner": "Planner",
|
| 512 |
+
"creative": "Creative Expert",
|
| 513 |
+
"technical": "Technical Expert",
|
| 514 |
+
"qa_tester": "QA Tester",
|
| 515 |
+
"research": "Research Analyst",
|
| 516 |
+
"security": "Security Reviewer",
|
| 517 |
+
"data_analyst": "Data Analyst",
|
| 518 |
+
}
|
| 519 |
+
# Reverse mapping: display label โ role key
|
| 520 |
+
_ROLE_LABEL_TO_KEY = {v: k for k, v in AGENT_ROLES.items()}
|
| 521 |
+
|
| 522 |
|
| 523 |
class WorkflowState(TypedDict):
|
| 524 |
"""Shared, inspectable state object threaded through the whole workflow."""
|
| 525 |
user_request: str
|
| 526 |
plan: str
|
| 527 |
+
current_role: str # "creative", "technical", "research", "security", or "data_analyst"
|
| 528 |
creative_output: str
|
| 529 |
technical_output: str
|
| 530 |
+
research_output: str
|
| 531 |
+
security_output: str
|
| 532 |
+
data_analyst_output: str
|
| 533 |
draft_output: str # latest specialist output forwarded to QA
|
| 534 |
qa_report: str
|
| 535 |
qa_passed: bool
|
|
|
|
| 543 |
"You are the Planner in a multi-role AI workflow.\n"
|
| 544 |
"Your job is to:\n"
|
| 545 |
"1. Break the user's task into clear subtasks.\n"
|
| 546 |
+
"2. Decide which specialist to call:\n"
|
| 547 |
+
" - 'Creative Expert' (ideas, framing, wording, brainstorming)\n"
|
| 548 |
+
" - 'Technical Expert' (code, architecture, implementation)\n"
|
| 549 |
+
" - 'Research Analyst' (information gathering, literature review, fact-finding)\n"
|
| 550 |
+
" - 'Security Reviewer' (security analysis, vulnerability checks, best practices)\n"
|
| 551 |
+
" - 'Data Analyst' (data analysis, statistics, pattern recognition, insights)\n"
|
| 552 |
"3. State clear success criteria.\n\n"
|
| 553 |
"Respond in this exact format:\n"
|
| 554 |
"TASK BREAKDOWN:\n<subtask list>\n\n"
|
| 555 |
+
"ROLE TO CALL: <Creative Expert | Technical Expert | Research Analyst | Security Reviewer | Data Analyst>\n\n"
|
| 556 |
"SUCCESS CRITERIA:\n<what a correct, complete answer looks like>\n\n"
|
| 557 |
"GUIDANCE FOR SPECIALIST:\n<any constraints or focus areas>"
|
| 558 |
)
|
|
|
|
| 593 |
"FINAL ANSWER:\n<the approved specialist output, reproduced in full>\n\n"
|
| 594 |
"If QA FAILED, respond with:\n"
|
| 595 |
"DECISION: REVISE\n"
|
| 596 |
+
"ROLE TO CALL: <Creative Expert | Technical Expert | Research Analyst | Security Reviewer | Data Analyst>\n"
|
| 597 |
"REVISED INSTRUCTIONS:\n<specific fixes the specialist must address>"
|
| 598 |
)
|
| 599 |
|
| 600 |
+
_RESEARCH_SYSTEM = (
|
| 601 |
+
"You are the Research Analyst in a multi-role AI workflow.\n"
|
| 602 |
+
"You gather information, review existing literature, and summarize facts relevant to the task.\n\n"
|
| 603 |
+
"Respond in this exact format:\n"
|
| 604 |
+
"SOURCES CONSULTED:\n<list of sources, references, or knowledge domains used>\n\n"
|
| 605 |
+
"KEY FINDINGS:\n<factual information gathered and synthesized>\n\n"
|
| 606 |
+
"RESEARCH SUMMARY:\n<a comprehensive summary of findings relevant to the request>"
|
| 607 |
+
)
|
| 608 |
+
|
| 609 |
+
_SECURITY_SYSTEM = (
|
| 610 |
+
"You are the Security Reviewer in a multi-role AI workflow.\n"
|
| 611 |
+
"You analyse outputs and plans for security vulnerabilities, risks, or best-practice violations.\n\n"
|
| 612 |
+
"Respond in this exact format:\n"
|
| 613 |
+
"SECURITY ANALYSIS:\n<identification of potential security concerns or risks>\n\n"
|
| 614 |
+
"VULNERABILITIES FOUND:\n<specific vulnerabilities or risks โ or 'None' if the output is secure>\n\n"
|
| 615 |
+
"RECOMMENDATIONS:\n<specific security improvements and mitigations>\n\n"
|
| 616 |
+
"REVIEWED OUTPUT:\n<the specialist output revised to address security concerns>"
|
| 617 |
+
)
|
| 618 |
+
|
| 619 |
+
_DATA_ANALYST_SYSTEM = (
|
| 620 |
+
"You are the Data Analyst in a multi-role AI workflow.\n"
|
| 621 |
+
"You analyse data, identify patterns, compute statistics, and provide actionable insights.\n\n"
|
| 622 |
+
"Respond in this exact format:\n"
|
| 623 |
+
"DATA OVERVIEW:\n<description of the data or problem being analysed>\n\n"
|
| 624 |
+
"ANALYSIS:\n<key patterns, statistics, or calculations>\n\n"
|
| 625 |
+
"INSIGHTS:\n<actionable conclusions drawn from the analysis>\n\n"
|
| 626 |
+
"ANALYTICAL DRAFT:\n<the complete analytical output or solution>"
|
| 627 |
+
)
|
| 628 |
+
|
| 629 |
|
| 630 |
# --- Internal helpers ---
|
| 631 |
|
|
|
|
| 642 |
"""Parse which specialist role the Planner wants to invoke.
|
| 643 |
|
| 644 |
Checks for the expected structured 'ROLE TO CALL:' format first,
|
| 645 |
+
then falls back to a word-boundary search.
|
| 646 |
Defaults to 'technical' when no clear signal is found.
|
| 647 |
"""
|
| 648 |
# Prefer the explicit structured label produced by the Planner prompt
|
|
|
|
| 650 |
return "creative"
|
| 651 |
if "ROLE TO CALL: Technical Expert" in text:
|
| 652 |
return "technical"
|
| 653 |
+
if "ROLE TO CALL: Research Analyst" in text:
|
| 654 |
+
return "research"
|
| 655 |
+
if "ROLE TO CALL: Security Reviewer" in text:
|
| 656 |
+
return "security"
|
| 657 |
+
if "ROLE TO CALL: Data Analyst" in text:
|
| 658 |
+
return "data_analyst"
|
| 659 |
+
# Fallback: word-boundary match
|
| 660 |
if re.search(r"\bcreative\b", text, re.IGNORECASE):
|
| 661 |
return "creative"
|
| 662 |
+
if re.search(r"\bresearch\b", text, re.IGNORECASE):
|
| 663 |
+
return "research"
|
| 664 |
+
if re.search(r"\bsecurity\b", text, re.IGNORECASE):
|
| 665 |
+
return "security"
|
| 666 |
+
if re.search(r"\bdata\s+analyst\b", text, re.IGNORECASE):
|
| 667 |
+
return "data_analyst"
|
| 668 |
return "technical"
|
| 669 |
|
| 670 |
|
|
|
|
| 794 |
return state
|
| 795 |
|
| 796 |
|
| 797 |
+
def _step_research(chat_model, state: WorkflowState, trace: List[str]) -> WorkflowState:
|
| 798 |
+
"""Research Analyst: gather information and produce a comprehensive research summary."""
|
| 799 |
+
trace.append("\nโโโ [RESEARCH ANALYST] Gathering information... โโโ")
|
| 800 |
+
content = (
|
| 801 |
+
f"User request: {state['user_request']}\n\n"
|
| 802 |
+
f"Planner instructions:\n{state['plan']}"
|
| 803 |
+
)
|
| 804 |
+
if state["revision_count"] > 0:
|
| 805 |
+
content += f"\n\nQA feedback to address:\n{state['qa_report']}"
|
| 806 |
+
text = _llm_call(chat_model, _RESEARCH_SYSTEM, content)
|
| 807 |
+
state["research_output"] = text
|
| 808 |
+
state["draft_output"] = text
|
| 809 |
+
trace.append(text)
|
| 810 |
+
trace.append("โโโ [RESEARCH ANALYST] Done โโโ")
|
| 811 |
+
return state
|
| 812 |
+
|
| 813 |
+
|
| 814 |
+
def _step_security(chat_model, state: WorkflowState, trace: List[str]) -> WorkflowState:
|
| 815 |
+
"""Security Reviewer: analyse output for vulnerabilities and produce a secure revision."""
|
| 816 |
+
trace.append("\nโโโ [SECURITY REVIEWER] Analysing for security issues... โโโ")
|
| 817 |
+
content = (
|
| 818 |
+
f"User request: {state['user_request']}\n\n"
|
| 819 |
+
f"Planner instructions:\n{state['plan']}"
|
| 820 |
+
)
|
| 821 |
+
if state["revision_count"] > 0:
|
| 822 |
+
content += f"\n\nQA feedback to address:\n{state['qa_report']}"
|
| 823 |
+
text = _llm_call(chat_model, _SECURITY_SYSTEM, content)
|
| 824 |
+
state["security_output"] = text
|
| 825 |
+
state["draft_output"] = text
|
| 826 |
+
trace.append(text)
|
| 827 |
+
trace.append("โโโ [SECURITY REVIEWER] Done โโโ")
|
| 828 |
+
return state
|
| 829 |
+
|
| 830 |
+
|
| 831 |
+
def _step_data_analyst(chat_model, state: WorkflowState, trace: List[str]) -> WorkflowState:
|
| 832 |
+
"""Data Analyst: analyse data, identify patterns, and produce actionable insights."""
|
| 833 |
+
trace.append("\nโโโ [DATA ANALYST] Analysing data and patterns... โโโ")
|
| 834 |
+
content = (
|
| 835 |
+
f"User request: {state['user_request']}\n\n"
|
| 836 |
+
f"Planner instructions:\n{state['plan']}"
|
| 837 |
+
)
|
| 838 |
+
if state["revision_count"] > 0:
|
| 839 |
+
content += f"\n\nQA feedback to address:\n{state['qa_report']}"
|
| 840 |
+
text = _llm_call(chat_model, _DATA_ANALYST_SYSTEM, content)
|
| 841 |
+
state["data_analyst_output"] = text
|
| 842 |
+
state["draft_output"] = text
|
| 843 |
+
trace.append(text)
|
| 844 |
+
trace.append("โโโ [DATA ANALYST] Done โโโ")
|
| 845 |
+
return state
|
| 846 |
+
|
| 847 |
+
|
| 848 |
+
# Mapping from role key โ step function, used by the orchestration loop
|
| 849 |
+
_SPECIALIST_STEPS = {
|
| 850 |
+
"creative": _step_creative,
|
| 851 |
+
"technical": _step_technical,
|
| 852 |
+
"research": _step_research,
|
| 853 |
+
"security": _step_security,
|
| 854 |
+
"data_analyst": _step_data_analyst,
|
| 855 |
+
}
|
| 856 |
+
|
| 857 |
+
|
| 858 |
# --- Specialist role tools ---
|
| 859 |
# These wrap the step functions as @tool so the Planner (or any LangChain agent)
|
| 860 |
# can invoke specialists in a standard tool-use pattern.
|
|
|
|
| 862 |
# Holds the active model ID for standalone specialist tool calls.
|
| 863 |
_workflow_model_id: str = DEFAULT_MODEL_ID
|
| 864 |
|
| 865 |
+
_EMPTY_STATE_BASE: WorkflowState = {
|
| 866 |
+
"user_request": "", "plan": "", "current_role": "",
|
| 867 |
+
"creative_output": "", "technical_output": "",
|
| 868 |
+
"research_output": "", "security_output": "", "data_analyst_output": "",
|
| 869 |
+
"draft_output": "", "qa_report": "", "qa_passed": False,
|
| 870 |
+
"revision_count": 0, "final_answer": "",
|
| 871 |
+
}
|
| 872 |
+
|
| 873 |
|
| 874 |
@tool
|
| 875 |
def call_creative_expert(task: str) -> str:
|
| 876 |
"""Call the Creative Expert to brainstorm ideas, framing, and produce a draft for a given task."""
|
| 877 |
chat = build_provider_chat(_workflow_model_id)
|
| 878 |
+
state: WorkflowState = {**_EMPTY_STATE_BASE, "user_request": task, "plan": task, "current_role": "creative"}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 879 |
state = _step_creative(chat, state, [])
|
| 880 |
return state["creative_output"]
|
| 881 |
|
|
|
|
| 884 |
def call_technical_expert(task: str) -> str:
|
| 885 |
"""Call the Technical Expert to produce implementation details and a solution for a given task."""
|
| 886 |
chat = build_provider_chat(_workflow_model_id)
|
| 887 |
+
state: WorkflowState = {**_EMPTY_STATE_BASE, "user_request": task, "plan": task, "current_role": "technical"}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 888 |
state = _step_technical(chat, state, [])
|
| 889 |
return state["technical_output"]
|
| 890 |
|
|
|
|
| 902 |
task = task_and_output
|
| 903 |
output = task_and_output
|
| 904 |
# current_role is left empty โ this is a standalone QA call outside the normal loop
|
| 905 |
+
state: WorkflowState = {**_EMPTY_STATE_BASE, "user_request": task, "plan": task, "draft_output": output}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 906 |
state = _step_qa(chat, state, [])
|
| 907 |
return state["qa_report"]
|
| 908 |
|
| 909 |
|
| 910 |
+
@tool
|
| 911 |
+
def call_research_analyst(task: str) -> str:
|
| 912 |
+
"""Call the Research Analyst to gather information and summarize findings for a given task."""
|
| 913 |
+
chat = build_provider_chat(_workflow_model_id)
|
| 914 |
+
state: WorkflowState = {**_EMPTY_STATE_BASE, "user_request": task, "plan": task, "current_role": "research"}
|
| 915 |
+
state = _step_research(chat, state, [])
|
| 916 |
+
return state["research_output"]
|
| 917 |
+
|
| 918 |
+
|
| 919 |
+
@tool
|
| 920 |
+
def call_security_reviewer(task: str) -> str:
|
| 921 |
+
"""Call the Security Reviewer to analyse output for vulnerabilities and security best practices."""
|
| 922 |
+
chat = build_provider_chat(_workflow_model_id)
|
| 923 |
+
state: WorkflowState = {**_EMPTY_STATE_BASE, "user_request": task, "plan": task, "current_role": "security"}
|
| 924 |
+
state = _step_security(chat, state, [])
|
| 925 |
+
return state["security_output"]
|
| 926 |
+
|
| 927 |
+
|
| 928 |
+
@tool
|
| 929 |
+
def call_data_analyst(task: str) -> str:
|
| 930 |
+
"""Call the Data Analyst to analyse data, identify patterns, and provide actionable insights."""
|
| 931 |
+
chat = build_provider_chat(_workflow_model_id)
|
| 932 |
+
state: WorkflowState = {**_EMPTY_STATE_BASE, "user_request": task, "plan": task, "current_role": "data_analyst"}
|
| 933 |
+
state = _step_data_analyst(chat, state, [])
|
| 934 |
+
return state["data_analyst_output"]
|
| 935 |
+
|
| 936 |
+
|
| 937 |
# --- Orchestration loop ---
|
| 938 |
|
| 939 |
+
def run_multi_role_workflow(
|
| 940 |
+
message: str,
|
| 941 |
+
model_id: str,
|
| 942 |
+
active_role_labels: Optional[List[str]] = None,
|
| 943 |
+
) -> Tuple[str, str]:
|
| 944 |
"""Run the supervisor-style multi-role workflow.
|
| 945 |
|
| 946 |
Flow:
|
| 947 |
+
1. Planner (if active) analyses the task and picks a specialist.
|
| 948 |
+
2. Specialist generates output; falls back to first active specialist if chosen one is disabled.
|
| 949 |
+
3. QA Tester (if active) reviews the output.
|
| 950 |
+
4. Planner (if active) reviews QA result and either approves or requests a revision.
|
| 951 |
5. Repeat from step 2 if QA fails and retries remain.
|
| 952 |
6. If max retries are reached, return best attempt with QA concerns.
|
| 953 |
|
| 954 |
+
Args:
|
| 955 |
+
message: The user's task or request.
|
| 956 |
+
model_id: HuggingFace model ID to use.
|
| 957 |
+
active_role_labels: Display names of active agent roles (e.g. ["Planner", "Technical Expert"]).
|
| 958 |
+
Defaults to all roles when None.
|
| 959 |
+
|
| 960 |
Returns:
|
| 961 |
(final_answer, workflow_trace_text)
|
| 962 |
"""
|
|
|
|
| 964 |
_workflow_model_id = model_id
|
| 965 |
chat_model = build_provider_chat(model_id)
|
| 966 |
|
| 967 |
+
# Resolve active role keys from display labels
|
| 968 |
+
if active_role_labels is None:
|
| 969 |
+
active_role_labels = list(AGENT_ROLES.values())
|
| 970 |
+
active_keys = {_ROLE_LABEL_TO_KEY[lbl] for lbl in active_role_labels if lbl in _ROLE_LABEL_TO_KEY}
|
| 971 |
+
|
| 972 |
+
# Determine which specialist keys are active (ordered list for deterministic fallback)
|
| 973 |
+
all_specialist_keys = ["creative", "technical", "research", "security", "data_analyst"]
|
| 974 |
+
active_specialist_keys = [k for k in all_specialist_keys if k in active_keys]
|
| 975 |
+
|
| 976 |
+
planner_active = "planner" in active_keys
|
| 977 |
+
qa_active = "qa_tester" in active_keys
|
| 978 |
+
|
| 979 |
+
if not active_specialist_keys:
|
| 980 |
+
return "No specialist agents are active. Please enable at least one specialist role.", ""
|
| 981 |
+
|
| 982 |
state: WorkflowState = {
|
| 983 |
"user_request": message,
|
| 984 |
"plan": "",
|
| 985 |
"current_role": "",
|
| 986 |
"creative_output": "",
|
| 987 |
"technical_output": "",
|
| 988 |
+
"research_output": "",
|
| 989 |
+
"security_output": "",
|
| 990 |
+
"data_analyst_output": "",
|
| 991 |
"draft_output": "",
|
| 992 |
"qa_report": "",
|
| 993 |
"qa_passed": False,
|
|
|
|
| 999 |
"โโโ MULTI-ROLE WORKFLOW STARTED โโโ",
|
| 1000 |
f"Model : {model_id}",
|
| 1001 |
f"Request : {message}",
|
| 1002 |
+
f"Active roles: {', '.join(active_role_labels)}",
|
| 1003 |
f"Max revisions: {MAX_REVISIONS}",
|
| 1004 |
]
|
| 1005 |
|
| 1006 |
try:
|
| 1007 |
+
if planner_active:
|
| 1008 |
+
# Step 1: Planner creates the initial plan
|
| 1009 |
+
state = _step_plan(chat_model, state, trace)
|
| 1010 |
+
else:
|
| 1011 |
+
# No planner: auto-select first active specialist
|
| 1012 |
+
state["current_role"] = active_specialist_keys[0]
|
| 1013 |
+
state["plan"] = message
|
| 1014 |
+
trace.append(
|
| 1015 |
+
f"\n[Planner disabled] Auto-routing to: {state['current_role'].upper()}"
|
| 1016 |
+
)
|
| 1017 |
|
| 1018 |
# Orchestration loop: specialist โ QA โ Planner review โ revise if needed
|
| 1019 |
while True:
|
| 1020 |
+
# Step 2: invoke the chosen specialist, falling back if that role is disabled
|
| 1021 |
+
role = state["current_role"]
|
| 1022 |
+
if role not in active_specialist_keys:
|
| 1023 |
+
role = active_specialist_keys[0]
|
| 1024 |
+
state["current_role"] = role
|
| 1025 |
+
trace.append(f" โ Requested role not active โ routing to {role.upper()}")
|
| 1026 |
+
|
| 1027 |
+
step_fn = _SPECIALIST_STEPS.get(role, _step_technical)
|
| 1028 |
+
state = step_fn(chat_model, state, trace)
|
| 1029 |
+
|
| 1030 |
+
# Step 3: QA reviews the specialist's draft (if enabled)
|
| 1031 |
+
if qa_active:
|
| 1032 |
+
state = _step_qa(chat_model, state, trace)
|
| 1033 |
else:
|
| 1034 |
+
state["qa_passed"] = True
|
| 1035 |
+
state["qa_report"] = "QA Tester is disabled โ skipping quality review."
|
| 1036 |
+
trace.append("\n[QA Tester disabled] Skipping quality review โ auto-pass.")
|
| 1037 |
+
|
| 1038 |
+
# Step 4: Planner reviews QA and either approves or schedules a revision
|
| 1039 |
+
if planner_active and qa_active:
|
| 1040 |
+
state = _step_planner_review(chat_model, state, trace)
|
| 1041 |
+
|
| 1042 |
+
# Exit if the Planner approved the result
|
| 1043 |
+
if state["final_answer"]:
|
| 1044 |
+
trace.append("\nโโโ WORKFLOW COMPLETE โ APPROVED โโโ")
|
| 1045 |
+
break
|
| 1046 |
+
|
| 1047 |
+
# Increment revision counter and enforce the retry limit
|
| 1048 |
+
state["revision_count"] += 1
|
| 1049 |
+
if state["revision_count"] >= MAX_REVISIONS:
|
| 1050 |
+
state["final_answer"] = state["draft_output"]
|
| 1051 |
+
trace.append(
|
| 1052 |
+
f"\nโโโ MAX REVISIONS REACHED ({MAX_REVISIONS}) โโโ\n"
|
| 1053 |
+
f"Returning best attempt. Outstanding QA concerns:\n{state['qa_report']}"
|
| 1054 |
+
)
|
| 1055 |
+
break
|
| 1056 |
|
| 1057 |
+
trace.append(f"\nโโโ REVISION {state['revision_count']} / {MAX_REVISIONS} โโโ")
|
| 1058 |
+
else:
|
| 1059 |
+
# No Planner review loop โ accept the draft as the final answer
|
| 1060 |
state["final_answer"] = state["draft_output"]
|
| 1061 |
+
trace.append("\nโโโ WORKFLOW COMPLETE โโโ")
|
|
|
|
|
|
|
|
|
|
| 1062 |
break
|
| 1063 |
|
|
|
|
|
|
|
| 1064 |
except Exception as exc:
|
| 1065 |
trace.append(f"\n[ERROR] {exc}\n{traceback.format_exc()}")
|
| 1066 |
state["final_answer"] = state["draft_output"] or f"Workflow error: {exc}"
|
|
|
|
| 1430 |
with gr.Tab("Multi-Role Workflow"):
|
| 1431 |
gr.Markdown(
|
| 1432 |
"## Supervisor-style Multi-Role Workflow\n"
|
| 1433 |
+
"**Planner** โ **Specialist** โ **QA Tester** โ **Planner review**\n\n"
|
| 1434 |
"The Planner breaks the task, picks the right specialist, and reviews QA feedback. "
|
| 1435 |
+
f"If QA fails, the loop repeats up to **{MAX_REVISIONS}** times before accepting the best attempt.\n\n"
|
| 1436 |
+
"Use the checkboxes on the right to enable or disable individual agent roles."
|
| 1437 |
)
|
| 1438 |
|
| 1439 |
with gr.Row():
|
|
|
|
| 1443 |
label="Model",
|
| 1444 |
)
|
| 1445 |
|
| 1446 |
+
with gr.Row():
|
| 1447 |
+
with gr.Column(scale=3):
|
| 1448 |
+
wf_input = gr.Textbox(
|
| 1449 |
+
label="Task / Request",
|
| 1450 |
+
placeholder=(
|
| 1451 |
+
"Describe what you want the multi-role team to work onโฆ\n"
|
| 1452 |
+
"e.g. 'Write a short blog post about the benefits of open-source AI'"
|
| 1453 |
+
),
|
| 1454 |
+
lines=3,
|
| 1455 |
+
)
|
| 1456 |
+
wf_submit_btn = gr.Button("โถ Run Multi-Role Workflow", variant="primary")
|
| 1457 |
+
|
| 1458 |
+
with gr.Column(scale=1):
|
| 1459 |
+
active_agents = gr.CheckboxGroup(
|
| 1460 |
+
choices=list(AGENT_ROLES.values()),
|
| 1461 |
+
value=list(AGENT_ROLES.values()),
|
| 1462 |
+
label="Active agent roles",
|
| 1463 |
+
)
|
| 1464 |
|
| 1465 |
with gr.Row():
|
| 1466 |
with gr.Column(scale=2):
|
|
|
|
| 1476 |
interactive=False,
|
| 1477 |
)
|
| 1478 |
|
| 1479 |
+
def _run_workflow_ui(
|
| 1480 |
+
message: str, model_id: str, role_labels: List[str]
|
| 1481 |
+
) -> Tuple[str, str]:
|
| 1482 |
"""Gradio handler: validate input, run the workflow, return outputs."""
|
| 1483 |
if not message or not message.strip():
|
| 1484 |
return "No input provided.", ""
|
| 1485 |
try:
|
| 1486 |
+
final_answer, trace = run_multi_role_workflow(
|
| 1487 |
+
message.strip(), model_id, role_labels
|
| 1488 |
+
)
|
| 1489 |
return final_answer, trace
|
| 1490 |
except Exception as exc:
|
| 1491 |
return f"Workflow error: {exc}", traceback.format_exc()
|
| 1492 |
|
| 1493 |
wf_submit_btn.click(
|
| 1494 |
fn=_run_workflow_ui,
|
| 1495 |
+
inputs=[wf_input, wf_model_dropdown, active_agents],
|
| 1496 |
outputs=[wf_answer, wf_trace],
|
| 1497 |
show_api=False,
|
| 1498 |
)
|
| 1499 |
|
| 1500 |
wf_input.submit(
|
| 1501 |
fn=_run_workflow_ui,
|
| 1502 |
+
inputs=[wf_input, wf_model_dropdown, active_agents],
|
| 1503 |
outputs=[wf_answer, wf_trace],
|
| 1504 |
show_api=False,
|
| 1505 |
)
|