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 files

Co-authored-by: CatoG <47473856+CatoG@users.noreply.github.com>

Files changed (1) hide show
  1. app.py +278 -69
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 "technical"
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: 'Creative Expert' (ideas, framing, wording)\n"
532
- " or 'Technical Expert' (code, architecture, implementation).\n"
 
 
 
 
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 for 'creative'.
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
- # Fallback: word-boundary match so 'creative' in 'not creative enough' still works,
606
- # but avoids false hits from unrelated use of the word.
 
 
 
 
 
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(message: str, model_id: str) -> Tuple[str, str]:
 
 
 
 
797
  """Run the supervisor-style multi-role workflow.
798
 
799
  Flow:
800
- 1. Planner analyses the task and picks a specialist.
801
- 2. Specialist (Creative or Technical) generates output.
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
- # Step 1: Planner creates the initial plan
836
- state = _step_plan(chat_model, state, trace)
 
 
 
 
 
 
 
 
837
 
838
  # Orchestration loop: specialist โ†’ QA โ†’ Planner review โ†’ revise if needed
839
  while True:
840
- # Step 2: invoke the chosen specialist
841
- if state["current_role"] == "creative":
842
- state = _step_creative(chat_model, state, trace)
 
 
 
 
 
 
 
 
 
 
843
  else:
844
- state = _step_technical(chat_model, state, trace)
845
-
846
- # Step 3: QA reviews the specialist's draft
847
- state = _step_qa(chat_model, state, trace)
848
-
849
- # Step 4: Planner reviews the QA result
850
- state = _step_planner_review(chat_model, state, trace)
851
-
852
- # Exit if the Planner approved the result
853
- if state["final_answer"]:
854
- trace.append("\nโ•โ•โ• WORKFLOW COMPLETE โ€” APPROVED โ•โ•โ•")
855
- break
 
 
 
 
 
 
 
 
 
 
856
 
857
- # Increment revision counter and enforce the retry limit
858
- state["revision_count"] += 1
859
- if state["revision_count"] >= MAX_REVISIONS:
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** (Creative or Technical) โ†’ **QA Tester** โ†’ **Planner review**\n\n"
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
- wf_input = gr.Textbox(
1251
- label="Task / Request",
1252
- placeholder=(
1253
- "Describe what you want the multi-role team to work onโ€ฆ\n"
1254
- "e.g. 'Write a short blog post about the benefits of open-source AI'"
1255
- ),
1256
- lines=3,
1257
- )
1258
- wf_submit_btn = gr.Button("โ–ถ Run Multi-Role Workflow", variant="primary")
 
 
 
 
 
 
 
 
 
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(message: str, model_id: str) -> Tuple[str, str]:
 
 
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(message.strip(), model_id)
 
 
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
  )