ShubhamSetia commited on
Commit
3d2961d
·
1 Parent(s): da51a9e

feat: add optional llm director

Browse files
app.py CHANGED
@@ -41,11 +41,13 @@ EMPTY_DIRECTOR_LOG = "No director notes yet."
41
  EMPTY_TRACE = "No trace events yet."
42
  EMPTY_BACKEND = (
43
  "Active backend: deterministic\n"
 
44
  "OpenBMB model id: openbmb/MiniCPM5-1B\n"
45
  "Model status: unloaded\n"
46
  "Fallback: deterministic safety path enabled"
47
  )
48
  BACKEND_CHOICES = ["deterministic", "openbmb"]
 
49
  OPENBMB_MODEL_ID = os.getenv("OPENBMB_MODEL_ID", DEFAULT_OPENBMB_MODEL_ID)
50
  DEFAULT_MAX_NEW_TOKENS = 80
51
  DEFAULT_TEMPERATURE = 0.8
@@ -1588,6 +1590,10 @@ def normalize_backend_name(backend_name: str | None) -> str:
1588
  return backend_name if backend_name in BACKEND_CHOICES else "deterministic"
1589
 
1590
 
 
 
 
 
1591
  def normalize_max_new_tokens(max_new_tokens: int | float | None) -> int:
1592
  if max_new_tokens is None:
1593
  return DEFAULT_MAX_NEW_TOKENS
@@ -1603,6 +1609,7 @@ def normalize_temperature(temperature: int | float | None) -> float:
1603
  def apply_backend_selection(
1604
  session: TheaterSession | None,
1605
  backend_name: str | None,
 
1606
  max_new_tokens: int | float | None = None,
1607
  temperature: int | float | None = None,
1608
  ) -> TheaterSession | None:
@@ -1610,6 +1617,7 @@ def apply_backend_selection(
1610
  return None
1611
  session.backend_name = normalize_backend_name(backend_name)
1612
  session.backend_model_id = OPENBMB_MODEL_ID if session.backend_name == "openbmb" else None
 
1613
  session.backend_max_new_tokens = normalize_max_new_tokens(max_new_tokens)
1614
  session.backend_temperature = normalize_temperature(temperature)
1615
  return session
@@ -1618,13 +1626,16 @@ def apply_backend_selection(
1618
  def render_backend_settings(
1619
  session: TheaterSession | None,
1620
  backend_name: str | None = None,
 
1621
  max_new_tokens: int | float | None = None,
1622
  temperature: int | float | None = None,
1623
  ) -> str:
1624
  selected_backend = normalize_backend_name(backend_name)
 
1625
  active_backend = session.backend_name if session is not None else selected_backend
 
1626
  model_id = session.backend_model_id if session is not None else None
1627
- if active_backend == "openbmb":
1628
  model_id = model_id or OPENBMB_MODEL_ID
1629
  status = get_backend_status(active_backend)
1630
  openbmb_status = get_backend_status("openbmb")
@@ -1638,7 +1649,9 @@ def render_backend_settings(
1638
  fallback_reason = status.latest_fallback_reason or "none"
1639
  return (
1640
  f"Active backend: {active_backend}\n"
1641
- "Available backends: deterministic, openbmb\n"
 
 
1642
  f"OpenBMB model id: {model_id or 'not selected'}\n"
1643
  f"Model status: {status.load_status}\n"
1644
  f"OpenBMB status: {openbmb_status.load_status}\n"
@@ -1663,11 +1676,13 @@ def create_show(
1663
  premise: str,
1664
  session: TheaterSession | None,
1665
  backend_name: str,
 
1666
  max_new_tokens: int | float,
1667
  temperature: int | float,
1668
  ):
1669
  premise = premise.strip()
1670
  selected_backend = normalize_backend_name(backend_name)
 
1671
  selected_max_new_tokens = normalize_max_new_tokens(max_new_tokens)
1672
  selected_temperature = normalize_temperature(temperature)
1673
  if not premise:
@@ -1677,7 +1692,13 @@ def create_show(
1677
  "No premise yet. Add a premise to raise the curtain.",
1678
  EMPTY_DIRECTOR_LOG,
1679
  EMPTY_TRACE,
1680
- render_backend_settings(None, selected_backend, selected_max_new_tokens, selected_temperature),
 
 
 
 
 
 
1681
  )
1682
 
1683
  session = create_show_from_premise(
@@ -1686,6 +1707,7 @@ def create_show(
1686
  backend_model_id=OPENBMB_MODEL_ID if selected_backend == "openbmb" else None,
1687
  backend_max_new_tokens=selected_max_new_tokens,
1688
  backend_temperature=selected_temperature,
 
1689
  )
1690
  return session, *render_outputs(session)
1691
 
@@ -1701,6 +1723,7 @@ def reset_show():
1701
  EMPTY_DIRECTOR_LOG,
1702
  EMPTY_TRACE,
1703
  "deterministic",
 
1704
  DEFAULT_MAX_NEW_TOKENS,
1705
  DEFAULT_TEMPERATURE,
1706
  True,
@@ -1711,6 +1734,7 @@ def reset_show():
1711
  def advance_one_beat(
1712
  session: TheaterSession | None,
1713
  backend_name: str,
 
1714
  max_new_tokens: int | float,
1715
  temperature: int | float,
1716
  ):
@@ -1721,10 +1745,10 @@ def advance_one_beat(
1721
  "Create a show before running a beat.",
1722
  EMPTY_DIRECTOR_LOG,
1723
  EMPTY_TRACE,
1724
- render_backend_settings(None, backend_name, max_new_tokens, temperature),
1725
  )
1726
 
1727
- session = apply_backend_selection(session, backend_name, max_new_tokens, temperature)
1728
  session = run_one_beat(session)
1729
  return session, *render_outputs(session)
1730
 
@@ -1732,6 +1756,7 @@ def advance_one_beat(
1732
  def advance_full_act(
1733
  session: TheaterSession | None,
1734
  backend_name: str,
 
1735
  max_new_tokens: int | float,
1736
  temperature: int | float,
1737
  use_deterministic_full_act: bool,
@@ -1743,34 +1768,42 @@ def advance_full_act(
1743
  "Create a show before running the full act.",
1744
  EMPTY_DIRECTOR_LOG,
1745
  EMPTY_TRACE,
1746
- render_backend_settings(None, backend_name, max_new_tokens, temperature),
1747
  )
1748
  return
1749
 
1750
- session = apply_backend_selection(session, backend_name, max_new_tokens, temperature)
1751
  selected_backend = session.backend_name
1752
- deterministic_full_act = selected_backend == "openbmb" and use_deterministic_full_act
 
 
 
 
1753
  if deterministic_full_act:
1754
  session.director_log.append(
1755
- "OpenBMB is selected, so Run Full Act will use deterministic actor lines for this playback."
1756
  )
1757
  session.trace_events.append("full_act_openbmb_deterministic_playback")
1758
 
1759
  if session.beat_index >= session.max_beats:
1760
  if deterministic_full_act:
1761
  session.backend_name = "deterministic"
 
1762
  session = run_one_beat(session)
1763
  if deterministic_full_act:
1764
  session.backend_name = selected_backend
 
1765
  yield session, *render_outputs(session)
1766
  return
1767
 
1768
  while session.beat_index < session.max_beats:
1769
  if deterministic_full_act:
1770
  session.backend_name = "deterministic"
 
1771
  session = run_one_beat(session)
1772
  if deterministic_full_act:
1773
  session.backend_name = selected_backend
 
1774
  yield session, *render_outputs(session)
1775
  if session.beat_index < session.max_beats:
1776
  sleep(PLAYBACK_DELAY_SECONDS)
@@ -1780,6 +1813,7 @@ def throw_audience_prop(
1780
  session: TheaterSession | None,
1781
  prop_name: str,
1782
  backend_name: str,
 
1783
  max_new_tokens: int | float,
1784
  temperature: int | float,
1785
  ):
@@ -1790,10 +1824,10 @@ def throw_audience_prop(
1790
  "Create a show before throwing a prop.",
1791
  EMPTY_DIRECTOR_LOG,
1792
  EMPTY_TRACE,
1793
- render_backend_settings(None, backend_name, max_new_tokens, temperature),
1794
  )
1795
 
1796
- session = apply_backend_selection(session, backend_name, max_new_tokens, temperature)
1797
  session = throw_prop(session, prop_name)
1798
  return session, *render_outputs(session)
1799
 
@@ -1802,6 +1836,7 @@ def summon_audience_actor(
1802
  session: TheaterSession | None,
1803
  actor_name: str,
1804
  backend_name: str,
 
1805
  max_new_tokens: int | float,
1806
  temperature: int | float,
1807
  ):
@@ -1812,10 +1847,10 @@ def summon_audience_actor(
1812
  "Create a show before summoning an actor.",
1813
  EMPTY_DIRECTOR_LOG,
1814
  EMPTY_TRACE,
1815
- render_backend_settings(None, backend_name, max_new_tokens, temperature),
1816
  )
1817
 
1818
- session = apply_backend_selection(session, backend_name, max_new_tokens, temperature)
1819
  session = summon_actor(session, actor_name)
1820
  return session, *render_outputs(session)
1821
 
@@ -1823,6 +1858,7 @@ def summon_audience_actor(
1823
  def request_audience_finale(
1824
  session: TheaterSession | None,
1825
  backend_name: str,
 
1826
  max_new_tokens: int | float,
1827
  temperature: int | float,
1828
  ):
@@ -1833,10 +1869,10 @@ def request_audience_finale(
1833
  "Create a show before requesting a finale.",
1834
  EMPTY_DIRECTOR_LOG,
1835
  EMPTY_TRACE,
1836
- render_backend_settings(None, backend_name, max_new_tokens, temperature),
1837
  )
1838
 
1839
- session = apply_backend_selection(session, backend_name, max_new_tokens, temperature)
1840
  session = request_finale(session)
1841
  return session, *render_outputs(session)
1842
 
@@ -1866,7 +1902,13 @@ def warm_up_backend(
1866
  session,
1867
  render_director_log(session),
1868
  render_trace(session),
1869
- render_backend_settings(session, "openbmb", selected_max_new_tokens, selected_temperature),
 
 
 
 
 
 
1870
  )
1871
 
1872
 
@@ -1962,6 +2004,12 @@ with gr.Blocks(title="AI Puppet Theater") as app:
1962
  label="Actor Line Backend",
1963
  interactive=True,
1964
  )
 
 
 
 
 
 
1965
  with gr.Row():
1966
  max_new_tokens_input = gr.Slider(
1967
  minimum=16,
@@ -1981,7 +2029,7 @@ with gr.Blocks(title="AI Puppet Theater") as app:
1981
  )
1982
  deterministic_full_act_input = gr.Checkbox(
1983
  value=True,
1984
- label="Use deterministic actor lines for OpenBMB full-act playback",
1985
  interactive=True,
1986
  )
1987
  warm_up_button = gr.Button("Warm up OpenBMB", elem_classes=["cue-action"])
@@ -1995,12 +2043,25 @@ with gr.Blocks(title="AI Puppet Theater") as app:
1995
 
1996
  create_button.click(
1997
  create_show,
1998
- inputs=[premise_input, session_state, backend_select, max_new_tokens_input, temperature_input],
 
 
 
 
 
 
 
1999
  outputs=[session_state, stage_output, transcript_output, director_output, trace_output, backend_output],
2000
  )
2001
  run_one_button.click(
2002
  advance_one_beat,
2003
- inputs=[session_state, backend_select, max_new_tokens_input, temperature_input],
 
 
 
 
 
 
2004
  outputs=[session_state, stage_output, transcript_output, director_output, trace_output, backend_output],
2005
  )
2006
  run_full_button.click(
@@ -2008,6 +2069,7 @@ with gr.Blocks(title="AI Puppet Theater") as app:
2008
  inputs=[
2009
  session_state,
2010
  backend_select,
 
2011
  max_new_tokens_input,
2012
  temperature_input,
2013
  deterministic_full_act_input,
@@ -2016,17 +2078,37 @@ with gr.Blocks(title="AI Puppet Theater") as app:
2016
  )
2017
  throw_prop_button.click(
2018
  throw_audience_prop,
2019
- inputs=[session_state, prop_input, backend_select, max_new_tokens_input, temperature_input],
 
 
 
 
 
 
 
2020
  outputs=[session_state, stage_output, transcript_output, director_output, trace_output, backend_output],
2021
  )
2022
  summon_actor_button.click(
2023
  summon_audience_actor,
2024
- inputs=[session_state, actor_input, backend_select, max_new_tokens_input, temperature_input],
 
 
 
 
 
 
 
2025
  outputs=[session_state, stage_output, transcript_output, director_output, trace_output, backend_output],
2026
  )
2027
  request_finale_button.click(
2028
  request_audience_finale,
2029
- inputs=[session_state, backend_select, max_new_tokens_input, temperature_input],
 
 
 
 
 
 
2030
  outputs=[session_state, stage_output, transcript_output, director_output, trace_output, backend_output],
2031
  )
2032
  warm_up_button.click(
@@ -2046,6 +2128,7 @@ with gr.Blocks(title="AI Puppet Theater") as app:
2046
  director_output,
2047
  trace_output,
2048
  backend_select,
 
2049
  max_new_tokens_input,
2050
  temperature_input,
2051
  deterministic_full_act_input,
 
41
  EMPTY_TRACE = "No trace events yet."
42
  EMPTY_BACKEND = (
43
  "Active backend: deterministic\n"
44
+ "Director mode: deterministic\n"
45
  "OpenBMB model id: openbmb/MiniCPM5-1B\n"
46
  "Model status: unloaded\n"
47
  "Fallback: deterministic safety path enabled"
48
  )
49
  BACKEND_CHOICES = ["deterministic", "openbmb"]
50
+ DIRECTOR_MODE_CHOICES = ["deterministic", "openbmb"]
51
  OPENBMB_MODEL_ID = os.getenv("OPENBMB_MODEL_ID", DEFAULT_OPENBMB_MODEL_ID)
52
  DEFAULT_MAX_NEW_TOKENS = 80
53
  DEFAULT_TEMPERATURE = 0.8
 
1590
  return backend_name if backend_name in BACKEND_CHOICES else "deterministic"
1591
 
1592
 
1593
+ def normalize_director_mode(director_mode: str | None) -> str:
1594
+ return director_mode if director_mode in DIRECTOR_MODE_CHOICES else "deterministic"
1595
+
1596
+
1597
  def normalize_max_new_tokens(max_new_tokens: int | float | None) -> int:
1598
  if max_new_tokens is None:
1599
  return DEFAULT_MAX_NEW_TOKENS
 
1609
  def apply_backend_selection(
1610
  session: TheaterSession | None,
1611
  backend_name: str | None,
1612
+ director_mode: str | None,
1613
  max_new_tokens: int | float | None = None,
1614
  temperature: int | float | None = None,
1615
  ) -> TheaterSession | None:
 
1617
  return None
1618
  session.backend_name = normalize_backend_name(backend_name)
1619
  session.backend_model_id = OPENBMB_MODEL_ID if session.backend_name == "openbmb" else None
1620
+ session.director_mode = normalize_director_mode(director_mode)
1621
  session.backend_max_new_tokens = normalize_max_new_tokens(max_new_tokens)
1622
  session.backend_temperature = normalize_temperature(temperature)
1623
  return session
 
1626
  def render_backend_settings(
1627
  session: TheaterSession | None,
1628
  backend_name: str | None = None,
1629
+ director_mode: str | None = None,
1630
  max_new_tokens: int | float | None = None,
1631
  temperature: int | float | None = None,
1632
  ) -> str:
1633
  selected_backend = normalize_backend_name(backend_name)
1634
+ selected_director_mode = normalize_director_mode(director_mode)
1635
  active_backend = session.backend_name if session is not None else selected_backend
1636
+ active_director_mode = session.director_mode if session is not None else selected_director_mode
1637
  model_id = session.backend_model_id if session is not None else None
1638
+ if active_backend == "openbmb" or active_director_mode == "openbmb":
1639
  model_id = model_id or OPENBMB_MODEL_ID
1640
  status = get_backend_status(active_backend)
1641
  openbmb_status = get_backend_status("openbmb")
 
1649
  fallback_reason = status.latest_fallback_reason or "none"
1650
  return (
1651
  f"Active backend: {active_backend}\n"
1652
+ f"Director mode: {active_director_mode}\n"
1653
+ "Available actor backends: deterministic, openbmb\n"
1654
+ "Available director modes: deterministic, openbmb\n"
1655
  f"OpenBMB model id: {model_id or 'not selected'}\n"
1656
  f"Model status: {status.load_status}\n"
1657
  f"OpenBMB status: {openbmb_status.load_status}\n"
 
1676
  premise: str,
1677
  session: TheaterSession | None,
1678
  backend_name: str,
1679
+ director_mode: str,
1680
  max_new_tokens: int | float,
1681
  temperature: int | float,
1682
  ):
1683
  premise = premise.strip()
1684
  selected_backend = normalize_backend_name(backend_name)
1685
+ selected_director_mode = normalize_director_mode(director_mode)
1686
  selected_max_new_tokens = normalize_max_new_tokens(max_new_tokens)
1687
  selected_temperature = normalize_temperature(temperature)
1688
  if not premise:
 
1692
  "No premise yet. Add a premise to raise the curtain.",
1693
  EMPTY_DIRECTOR_LOG,
1694
  EMPTY_TRACE,
1695
+ render_backend_settings(
1696
+ None,
1697
+ selected_backend,
1698
+ selected_director_mode,
1699
+ selected_max_new_tokens,
1700
+ selected_temperature,
1701
+ ),
1702
  )
1703
 
1704
  session = create_show_from_premise(
 
1707
  backend_model_id=OPENBMB_MODEL_ID if selected_backend == "openbmb" else None,
1708
  backend_max_new_tokens=selected_max_new_tokens,
1709
  backend_temperature=selected_temperature,
1710
+ director_mode=selected_director_mode,
1711
  )
1712
  return session, *render_outputs(session)
1713
 
 
1723
  EMPTY_DIRECTOR_LOG,
1724
  EMPTY_TRACE,
1725
  "deterministic",
1726
+ "deterministic",
1727
  DEFAULT_MAX_NEW_TOKENS,
1728
  DEFAULT_TEMPERATURE,
1729
  True,
 
1734
  def advance_one_beat(
1735
  session: TheaterSession | None,
1736
  backend_name: str,
1737
+ director_mode: str,
1738
  max_new_tokens: int | float,
1739
  temperature: int | float,
1740
  ):
 
1745
  "Create a show before running a beat.",
1746
  EMPTY_DIRECTOR_LOG,
1747
  EMPTY_TRACE,
1748
+ render_backend_settings(None, backend_name, director_mode, max_new_tokens, temperature),
1749
  )
1750
 
1751
+ session = apply_backend_selection(session, backend_name, director_mode, max_new_tokens, temperature)
1752
  session = run_one_beat(session)
1753
  return session, *render_outputs(session)
1754
 
 
1756
  def advance_full_act(
1757
  session: TheaterSession | None,
1758
  backend_name: str,
1759
+ director_mode: str,
1760
  max_new_tokens: int | float,
1761
  temperature: int | float,
1762
  use_deterministic_full_act: bool,
 
1768
  "Create a show before running the full act.",
1769
  EMPTY_DIRECTOR_LOG,
1770
  EMPTY_TRACE,
1771
+ render_backend_settings(None, backend_name, director_mode, max_new_tokens, temperature),
1772
  )
1773
  return
1774
 
1775
+ session = apply_backend_selection(session, backend_name, director_mode, max_new_tokens, temperature)
1776
  selected_backend = session.backend_name
1777
+ selected_director_mode = session.director_mode
1778
+ deterministic_full_act = (
1779
+ (selected_backend == "openbmb" or selected_director_mode == "openbmb")
1780
+ and use_deterministic_full_act
1781
+ )
1782
  if deterministic_full_act:
1783
  session.director_log.append(
1784
+ "OpenBMB is selected, so Run Full Act will use deterministic playback for this run."
1785
  )
1786
  session.trace_events.append("full_act_openbmb_deterministic_playback")
1787
 
1788
  if session.beat_index >= session.max_beats:
1789
  if deterministic_full_act:
1790
  session.backend_name = "deterministic"
1791
+ session.director_mode = "deterministic"
1792
  session = run_one_beat(session)
1793
  if deterministic_full_act:
1794
  session.backend_name = selected_backend
1795
+ session.director_mode = selected_director_mode
1796
  yield session, *render_outputs(session)
1797
  return
1798
 
1799
  while session.beat_index < session.max_beats:
1800
  if deterministic_full_act:
1801
  session.backend_name = "deterministic"
1802
+ session.director_mode = "deterministic"
1803
  session = run_one_beat(session)
1804
  if deterministic_full_act:
1805
  session.backend_name = selected_backend
1806
+ session.director_mode = selected_director_mode
1807
  yield session, *render_outputs(session)
1808
  if session.beat_index < session.max_beats:
1809
  sleep(PLAYBACK_DELAY_SECONDS)
 
1813
  session: TheaterSession | None,
1814
  prop_name: str,
1815
  backend_name: str,
1816
+ director_mode: str,
1817
  max_new_tokens: int | float,
1818
  temperature: int | float,
1819
  ):
 
1824
  "Create a show before throwing a prop.",
1825
  EMPTY_DIRECTOR_LOG,
1826
  EMPTY_TRACE,
1827
+ render_backend_settings(None, backend_name, director_mode, max_new_tokens, temperature),
1828
  )
1829
 
1830
+ session = apply_backend_selection(session, backend_name, director_mode, max_new_tokens, temperature)
1831
  session = throw_prop(session, prop_name)
1832
  return session, *render_outputs(session)
1833
 
 
1836
  session: TheaterSession | None,
1837
  actor_name: str,
1838
  backend_name: str,
1839
+ director_mode: str,
1840
  max_new_tokens: int | float,
1841
  temperature: int | float,
1842
  ):
 
1847
  "Create a show before summoning an actor.",
1848
  EMPTY_DIRECTOR_LOG,
1849
  EMPTY_TRACE,
1850
+ render_backend_settings(None, backend_name, director_mode, max_new_tokens, temperature),
1851
  )
1852
 
1853
+ session = apply_backend_selection(session, backend_name, director_mode, max_new_tokens, temperature)
1854
  session = summon_actor(session, actor_name)
1855
  return session, *render_outputs(session)
1856
 
 
1858
  def request_audience_finale(
1859
  session: TheaterSession | None,
1860
  backend_name: str,
1861
+ director_mode: str,
1862
  max_new_tokens: int | float,
1863
  temperature: int | float,
1864
  ):
 
1869
  "Create a show before requesting a finale.",
1870
  EMPTY_DIRECTOR_LOG,
1871
  EMPTY_TRACE,
1872
+ render_backend_settings(None, backend_name, director_mode, max_new_tokens, temperature),
1873
  )
1874
 
1875
+ session = apply_backend_selection(session, backend_name, director_mode, max_new_tokens, temperature)
1876
  session = request_finale(session)
1877
  return session, *render_outputs(session)
1878
 
 
1902
  session,
1903
  render_director_log(session),
1904
  render_trace(session),
1905
+ render_backend_settings(
1906
+ session,
1907
+ "openbmb",
1908
+ session.director_mode if session is not None else "deterministic",
1909
+ selected_max_new_tokens,
1910
+ selected_temperature,
1911
+ ),
1912
  )
1913
 
1914
 
 
2004
  label="Actor Line Backend",
2005
  interactive=True,
2006
  )
2007
+ director_mode_select = gr.Dropdown(
2008
+ choices=DIRECTOR_MODE_CHOICES,
2009
+ value="deterministic",
2010
+ label="Director Mode",
2011
+ interactive=True,
2012
+ )
2013
  with gr.Row():
2014
  max_new_tokens_input = gr.Slider(
2015
  minimum=16,
 
2029
  )
2030
  deterministic_full_act_input = gr.Checkbox(
2031
  value=True,
2032
+ label="Use deterministic playback for OpenBMB full-act runs",
2033
  interactive=True,
2034
  )
2035
  warm_up_button = gr.Button("Warm up OpenBMB", elem_classes=["cue-action"])
 
2043
 
2044
  create_button.click(
2045
  create_show,
2046
+ inputs=[
2047
+ premise_input,
2048
+ session_state,
2049
+ backend_select,
2050
+ director_mode_select,
2051
+ max_new_tokens_input,
2052
+ temperature_input,
2053
+ ],
2054
  outputs=[session_state, stage_output, transcript_output, director_output, trace_output, backend_output],
2055
  )
2056
  run_one_button.click(
2057
  advance_one_beat,
2058
+ inputs=[
2059
+ session_state,
2060
+ backend_select,
2061
+ director_mode_select,
2062
+ max_new_tokens_input,
2063
+ temperature_input,
2064
+ ],
2065
  outputs=[session_state, stage_output, transcript_output, director_output, trace_output, backend_output],
2066
  )
2067
  run_full_button.click(
 
2069
  inputs=[
2070
  session_state,
2071
  backend_select,
2072
+ director_mode_select,
2073
  max_new_tokens_input,
2074
  temperature_input,
2075
  deterministic_full_act_input,
 
2078
  )
2079
  throw_prop_button.click(
2080
  throw_audience_prop,
2081
+ inputs=[
2082
+ session_state,
2083
+ prop_input,
2084
+ backend_select,
2085
+ director_mode_select,
2086
+ max_new_tokens_input,
2087
+ temperature_input,
2088
+ ],
2089
  outputs=[session_state, stage_output, transcript_output, director_output, trace_output, backend_output],
2090
  )
2091
  summon_actor_button.click(
2092
  summon_audience_actor,
2093
+ inputs=[
2094
+ session_state,
2095
+ actor_input,
2096
+ backend_select,
2097
+ director_mode_select,
2098
+ max_new_tokens_input,
2099
+ temperature_input,
2100
+ ],
2101
  outputs=[session_state, stage_output, transcript_output, director_output, trace_output, backend_output],
2102
  )
2103
  request_finale_button.click(
2104
  request_audience_finale,
2105
+ inputs=[
2106
+ session_state,
2107
+ backend_select,
2108
+ director_mode_select,
2109
+ max_new_tokens_input,
2110
+ temperature_input,
2111
+ ],
2112
  outputs=[session_state, stage_output, transcript_output, director_output, trace_output, backend_output],
2113
  )
2114
  warm_up_button.click(
 
2128
  director_output,
2129
  trace_output,
2130
  backend_select,
2131
+ director_mode_select,
2132
  max_new_tokens_input,
2133
  temperature_input,
2134
  deterministic_full_act_input,
puppet_theater/__init__.py CHANGED
@@ -9,7 +9,15 @@ from puppet_theater.backends import (
9
  parse_actor_output,
10
  warm_up_openbmb,
11
  )
12
- from puppet_theater.director import BEAT_ARC, DirectorPolicy, run_full_act, run_one_beat
 
 
 
 
 
 
 
 
13
  from puppet_theater.models import Actor, ActorResponse, Beat, DirectorDecision, TheaterSession
14
  from puppet_theater.session import create_show_from_premise
15
 
@@ -21,11 +29,14 @@ __all__ = [
21
  "DEFAULT_OPENBMB_MODEL_ID",
22
  "DeterministicBackend",
23
  "DirectorDecision",
 
24
  "DirectorPolicy",
25
  "ModelBackend",
 
26
  "OpenBMBTransformersBackend",
27
  "TheaterSession",
28
  "create_show_from_premise",
 
29
  "generate_actor_response",
30
  "get_backend_status",
31
  "parse_actor_output",
 
9
  parse_actor_output,
10
  warm_up_openbmb,
11
  )
12
+ from puppet_theater.director import (
13
+ BEAT_ARC,
14
+ DirectorGeneration,
15
+ DirectorPolicy,
16
+ OpenBMBDirectorPolicy,
17
+ choose_director_decision,
18
+ run_full_act,
19
+ run_one_beat,
20
+ )
21
  from puppet_theater.models import Actor, ActorResponse, Beat, DirectorDecision, TheaterSession
22
  from puppet_theater.session import create_show_from_premise
23
 
 
29
  "DEFAULT_OPENBMB_MODEL_ID",
30
  "DeterministicBackend",
31
  "DirectorDecision",
32
+ "DirectorGeneration",
33
  "DirectorPolicy",
34
  "ModelBackend",
35
+ "OpenBMBDirectorPolicy",
36
  "OpenBMBTransformersBackend",
37
  "TheaterSession",
38
  "create_show_from_premise",
39
+ "choose_director_decision",
40
  "generate_actor_response",
41
  "get_backend_status",
42
  "parse_actor_output",
puppet_theater/director.py CHANGED
@@ -1,7 +1,18 @@
1
  from collections import Counter
 
 
 
2
 
3
- from puppet_theater.backends import generate_actor_response
 
 
 
 
 
 
 
4
  from puppet_theater.models import Actor, Beat, DirectorDecision, TheaterSession
 
5
 
6
 
7
  BEAT_ARC = [
@@ -14,6 +25,18 @@ BEAT_ARC = [
14
  ]
15
 
16
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  class DirectorPolicy:
18
  def decide(self, session: TheaterSession) -> DirectorDecision:
19
  if session.finale_requested or session.beat_index >= session.max_beats - 1:
@@ -100,6 +123,109 @@ class DirectorPolicy:
100
  }[beat_type]
101
 
102
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
  def run_one_beat(session: TheaterSession | None) -> TheaterSession | None:
104
  if session is None:
105
  return None
@@ -109,7 +235,8 @@ def run_one_beat(session: TheaterSession | None) -> TheaterSession | None:
109
  session.trace_events.append("beat_skipped:curtain_already_fallen")
110
  return session
111
 
112
- decision = DirectorPolicy().decide(session)
 
113
  speaker = _actor_by_name(session, decision.next_speaker)
114
  prop = session.latest_prop if decision.uses_prop else None
115
  if prop is not None:
@@ -119,14 +246,32 @@ def run_one_beat(session: TheaterSession | None) -> TheaterSession | None:
119
  "Director decision: "
120
  f"{decision.beat_type} for {speaker.name}; {decision.reason_summary}"
121
  )
 
 
 
 
 
 
 
 
 
 
122
  session.trace_events.append(
123
  "director_decision:"
 
124
  f"speaker={speaker.name}:"
125
  f"beat={decision.beat_type}:"
126
  f"uses_prop={decision.uses_prop}:"
127
  f"reveal_secret={decision.reveal_secret}:"
128
- f"should_end={decision.should_end_scene}"
 
 
 
 
 
129
  )
 
 
130
 
131
  backend_generation = generate_actor_response(session, decision, speaker, prop)
132
  session.beat_index += 1
@@ -204,3 +349,108 @@ def _actor_by_name(session: TheaterSession, actor_name: str) -> Actor:
204
  if actor.name == actor_name:
205
  return actor
206
  return session.actors[session.beat_index % len(session.actors)]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  from collections import Counter
2
+ from dataclasses import dataclass
3
+ import json
4
+ import time
5
 
6
+ from pydantic import ValidationError
7
+
8
+ from puppet_theater.backends import (
9
+ DEFAULT_OPENBMB_MODEL_ID,
10
+ OpenBMBTransformersBackend,
11
+ generate_actor_response,
12
+ get_backend,
13
+ )
14
  from puppet_theater.models import Actor, Beat, DirectorDecision, TheaterSession
15
+ from puppet_theater.prompts import DIRECTOR_DECISION_PROMPT
16
 
17
 
18
  BEAT_ARC = [
 
25
  ]
26
 
27
 
28
+ @dataclass(frozen=True)
29
+ class DirectorGeneration:
30
+ decision: DirectorDecision
31
+ director_mode: str
32
+ model_id: str | None
33
+ validation_status: str
34
+ fallback_used: bool
35
+ reason_summary: str
36
+ latency_ms: int | None = None
37
+ error: str | None = None
38
+
39
+
40
  class DirectorPolicy:
41
  def decide(self, session: TheaterSession) -> DirectorDecision:
42
  if session.finale_requested or session.beat_index >= session.max_beats - 1:
 
123
  }[beat_type]
124
 
125
 
126
+ class OpenBMBDirectorPolicy:
127
+ def __init__(self, fallback_policy: DirectorPolicy | None = None) -> None:
128
+ self.fallback_policy = fallback_policy or DirectorPolicy()
129
+
130
+ def decide(self, session: TheaterSession) -> DirectorGeneration:
131
+ start_time = time.perf_counter()
132
+ backend = get_backend(
133
+ "openbmb",
134
+ max_new_tokens=session.backend_max_new_tokens,
135
+ temperature=session.backend_temperature,
136
+ )
137
+ if not isinstance(backend, OpenBMBTransformersBackend):
138
+ return self._fallback(session, "backend_unavailable", start_time)
139
+
140
+ prompt = build_director_prompt(session)
141
+ try:
142
+ raw_output = backend._generate_text(prompt)
143
+ except Exception as exc:
144
+ return self._fallback(session, "backend_error", start_time, _summarize_error(exc), backend.model_id)
145
+
146
+ decision, validation_status = parse_director_decision(raw_output, session)
147
+ if decision is not None:
148
+ return DirectorGeneration(
149
+ decision=decision,
150
+ director_mode="openbmb",
151
+ model_id=backend.model_id,
152
+ validation_status=validation_status,
153
+ fallback_used=False,
154
+ reason_summary=decision.reason_summary,
155
+ latency_ms=_elapsed_ms(start_time),
156
+ )
157
+
158
+ repair_prompt = (
159
+ f"{prompt}\n\nPrevious output failed validation with status {validation_status}.\n"
160
+ "Return only valid JSON matching the required schema. No markdown. No extra keys.\n"
161
+ f"Previous output: {raw_output}"
162
+ )
163
+ try:
164
+ repair_output = backend._generate_text(repair_prompt)
165
+ except Exception as exc:
166
+ return self._fallback(
167
+ session,
168
+ f"{validation_status};repair_backend_error",
169
+ start_time,
170
+ _summarize_error(exc),
171
+ backend.model_id,
172
+ )
173
+
174
+ decision, repair_status = parse_director_decision(repair_output, session)
175
+ if decision is not None:
176
+ return DirectorGeneration(
177
+ decision=decision,
178
+ director_mode="openbmb",
179
+ model_id=backend.model_id,
180
+ validation_status=f"repair_{repair_status}",
181
+ fallback_used=False,
182
+ reason_summary=decision.reason_summary,
183
+ latency_ms=_elapsed_ms(start_time),
184
+ )
185
+
186
+ return self._fallback(
187
+ session,
188
+ f"{validation_status};repair_{repair_status}",
189
+ start_time,
190
+ model_id=backend.model_id,
191
+ )
192
+
193
+ def _fallback(
194
+ self,
195
+ session: TheaterSession,
196
+ validation_status: str,
197
+ start_time: float,
198
+ error: str | None = None,
199
+ model_id: str | None = None,
200
+ ) -> DirectorGeneration:
201
+ decision = self.fallback_policy.decide(session)
202
+ return DirectorGeneration(
203
+ decision=decision,
204
+ director_mode="openbmb",
205
+ model_id=model_id or DEFAULT_OPENBMB_MODEL_ID,
206
+ validation_status=validation_status,
207
+ fallback_used=True,
208
+ reason_summary=decision.reason_summary,
209
+ latency_ms=_elapsed_ms(start_time),
210
+ error=error,
211
+ )
212
+
213
+
214
+ def choose_director_decision(session: TheaterSession) -> DirectorGeneration:
215
+ if session.director_mode == "openbmb":
216
+ return OpenBMBDirectorPolicy().decide(session)
217
+ decision = DirectorPolicy().decide(session)
218
+ return DirectorGeneration(
219
+ decision=decision,
220
+ director_mode="deterministic",
221
+ model_id=None,
222
+ validation_status="valid",
223
+ fallback_used=False,
224
+ reason_summary=decision.reason_summary,
225
+ latency_ms=0,
226
+ )
227
+
228
+
229
  def run_one_beat(session: TheaterSession | None) -> TheaterSession | None:
230
  if session is None:
231
  return None
 
235
  session.trace_events.append("beat_skipped:curtain_already_fallen")
236
  return session
237
 
238
+ director_generation = choose_director_decision(session)
239
+ decision = director_generation.decision
240
  speaker = _actor_by_name(session, decision.next_speaker)
241
  prop = session.latest_prop if decision.uses_prop else None
242
  if prop is not None:
 
246
  "Director decision: "
247
  f"{decision.beat_type} for {speaker.name}; {decision.reason_summary}"
248
  )
249
+ session.director_log.append(
250
+ "Director mode "
251
+ f"{director_generation.director_mode} "
252
+ f"({director_generation.validation_status}, fallback={director_generation.fallback_used}, "
253
+ f"latency={director_generation.latency_ms}ms)."
254
+ )
255
+ if director_generation.model_id:
256
+ session.director_log.append(f"Director model id: {director_generation.model_id}.")
257
+ if director_generation.error:
258
+ session.director_log.append(f"Director fallback reason: {director_generation.error}.")
259
  session.trace_events.append(
260
  "director_decision:"
261
+ f"mode={director_generation.director_mode}:"
262
  f"speaker={speaker.name}:"
263
  f"beat={decision.beat_type}:"
264
  f"uses_prop={decision.uses_prop}:"
265
  f"reveal_secret={decision.reveal_secret}:"
266
+ f"should_end={decision.should_end_scene}:"
267
+ f"validation={director_generation.validation_status}:"
268
+ f"fallback={director_generation.fallback_used}:"
269
+ f"model={director_generation.model_id or 'none'}:"
270
+ f"latency_ms={director_generation.latency_ms}:"
271
+ f"reason={_trace_text(director_generation.reason_summary)}"
272
  )
273
+ if director_generation.fallback_used:
274
+ session.trace_events.append(f"director_fallback_used:{director_generation.validation_status}")
275
 
276
  backend_generation = generate_actor_response(session, decision, speaker, prop)
277
  session.beat_index += 1
 
349
  if actor.name == actor_name:
350
  return actor
351
  return session.actors[session.beat_index % len(session.actors)]
352
+
353
+
354
+ def build_director_prompt(session: TheaterSession) -> str:
355
+ actor_profiles = "\n".join(
356
+ "- "
357
+ f"{actor.name}: goal={actor.goal}; style={actor.speaking_style}; "
358
+ f"secret_available={session.beat_index >= 2}"
359
+ for actor in session.actors
360
+ )
361
+ recent_transcript = "\n".join(
362
+ f"{beat.speaker}: {beat.line}" for beat in session.transcript[-4:]
363
+ ) or "No lines yet."
364
+ summoned_actors = ", ".join(actor.name for actor in session.actors[3:]) or "None"
365
+ return DIRECTOR_DECISION_PROMPT.format(
366
+ show_title=session.show_title,
367
+ premise=session.premise,
368
+ setting=session.setting,
369
+ beat_index=session.beat_index,
370
+ max_beats=session.max_beats,
371
+ allowed_beat_types=", ".join(BEAT_ARC),
372
+ actor_profiles=actor_profiles,
373
+ recent_transcript=recent_transcript,
374
+ audience_action=session.latest_audience_action or "None",
375
+ props=", ".join(session.props) or "None",
376
+ latest_prop=session.latest_prop or "None",
377
+ summoned_actors=summoned_actors,
378
+ finale_requested=session.finale_requested,
379
+ )
380
+
381
+
382
+ def parse_director_decision(raw_output: object, session: TheaterSession) -> tuple[DirectorDecision | None, str]:
383
+ parsed = _coerce_director_output(raw_output)
384
+ if parsed is None:
385
+ return None, "invalid_schema"
386
+ if "speaker_name" in parsed and "next_speaker" not in parsed:
387
+ parsed["next_speaker"] = parsed["speaker_name"]
388
+ try:
389
+ decision = DirectorDecision.model_validate(parsed)
390
+ except ValidationError:
391
+ return None, "invalid_required_fields"
392
+ return validate_director_decision(decision, session)
393
+
394
+
395
+ def validate_director_decision(decision: DirectorDecision, session: TheaterSession) -> tuple[DirectorDecision | None, str]:
396
+ actor_names = {actor.name for actor in session.actors}
397
+ if decision.next_speaker not in actor_names:
398
+ return None, "invalid_speaker"
399
+ if decision.beat_type not in BEAT_ARC:
400
+ return None, "invalid_beat_type"
401
+ if len(decision.instruction) > 240 or len(decision.reason_summary) > 240:
402
+ return None, "invalid_too_long"
403
+
404
+ update: dict[str, object] = {}
405
+ if session.finale_requested or session.beat_index >= session.max_beats - 1:
406
+ update["beat_type"] = "finale"
407
+ update["should_end_scene"] = True
408
+ update["stage_effect"] = "curtain_fall"
409
+ elif decision.should_end_scene and session.beat_index < session.max_beats - 2:
410
+ update["should_end_scene"] = False
411
+ if decision.uses_prop and session.latest_prop is None:
412
+ update["uses_prop"] = False
413
+ if decision.beat_type == "secret_reveal":
414
+ update["reveal_secret"] = True
415
+
416
+ if update:
417
+ decision = decision.model_copy(update=update)
418
+ return decision, "valid"
419
+
420
+
421
+ def _coerce_director_output(raw_output: object) -> dict[str, object] | None:
422
+ if isinstance(raw_output, DirectorDecision):
423
+ return raw_output.model_dump()
424
+ if isinstance(raw_output, dict):
425
+ return raw_output
426
+ if isinstance(raw_output, str):
427
+ text = raw_output.strip()
428
+ if not text:
429
+ return None
430
+ if text.startswith("```"):
431
+ text = text.strip("`")
432
+ if "\n" in text:
433
+ text = text.split("\n", maxsplit=1)[1]
434
+ start = text.find("{")
435
+ end = text.rfind("}")
436
+ if start != -1 and end != -1 and end > start:
437
+ text = text[start : end + 1]
438
+ try:
439
+ decoded = json.loads(text)
440
+ except json.JSONDecodeError:
441
+ return None
442
+ return decoded if isinstance(decoded, dict) else None
443
+ return None
444
+
445
+
446
+ def _elapsed_ms(start_time: float) -> int:
447
+ return round((time.perf_counter() - start_time) * 1000)
448
+
449
+
450
+ def _summarize_error(exc: Exception) -> str:
451
+ message = " ".join(str(exc).split()) or exc.__class__.__name__
452
+ return message[:180]
453
+
454
+
455
+ def _trace_text(value: str) -> str:
456
+ return " ".join(value.split()).replace(":", "-")[:140]
puppet_theater/models.py CHANGED
@@ -57,6 +57,11 @@ class DirectorDecision(BaseModel):
57
  raise ValueError("field must not be empty")
58
  return cleaned
59
 
 
 
 
 
 
60
 
61
  @dataclass
62
  class Actor:
@@ -98,5 +103,6 @@ class TheaterSession:
98
  backend_model_id: str | None = None
99
  backend_max_new_tokens: int = 80
100
  backend_temperature: float = 0.8
 
101
  # One-shot: show opening-curtain animation on the first stage render after create.
102
  play_opening_curtain: bool = False
 
57
  raise ValueError("field must not be empty")
58
  return cleaned
59
 
60
+ @field_validator("instruction", "reason_summary")
61
+ @classmethod
62
+ def keep_brief(cls, value: str) -> str:
63
+ return value[:240].rstrip()
64
+
65
 
66
  @dataclass
67
  class Actor:
 
103
  backend_model_id: str | None = None
104
  backend_max_new_tokens: int = 80
105
  backend_temperature: float = 0.8
106
+ director_mode: str = "deterministic"
107
  # One-shot: show opening-curtain animation on the first stage render after create.
108
  play_opening_curtain: bool = False
puppet_theater/prompts.py CHANGED
@@ -28,18 +28,28 @@ Choose the next beat for a short puppet scene. Keep the scene tight, rotate spea
28
  respect finale requests, and avoid exposing actor secrets unless the beat calls for it.
29
 
30
  Return only JSON with these fields:
 
31
  - beat_type
32
- - speaker_name
 
 
 
33
  - reason_summary
34
- - should_end
35
 
36
  Show title: {show_title}
37
  Premise: {premise}
 
38
  Current beat: {beat_index}
39
  Maximum beats: {max_beats}
40
- Available actors: {actor_names}
 
 
41
  Recent transcript: {recent_transcript}
42
  Audience action: {audience_action}
 
 
 
43
  Finale requested: {finale_requested}
44
  """
45
 
 
28
  respect finale requests, and avoid exposing actor secrets unless the beat calls for it.
29
 
30
  Return only JSON with these fields:
31
+ - next_speaker
32
  - beat_type
33
+ - instruction
34
+ - stage_effect
35
+ - uses_prop
36
+ - reveal_secret
37
  - reason_summary
38
+ - should_end_scene
39
 
40
  Show title: {show_title}
41
  Premise: {premise}
42
+ Setting: {setting}
43
  Current beat: {beat_index}
44
  Maximum beats: {max_beats}
45
+ Allowed beat types: {allowed_beat_types}
46
+ Available actors:
47
+ {actor_profiles}
48
  Recent transcript: {recent_transcript}
49
  Audience action: {audience_action}
50
+ Props on stage: {props}
51
+ Latest prop: {latest_prop}
52
+ Summoned actors: {summoned_actors}
53
  Finale requested: {finale_requested}
54
  """
55
 
puppet_theater/session.py CHANGED
@@ -33,6 +33,7 @@ def create_show_from_premise(
33
  backend_model_id: str | None = None,
34
  backend_max_new_tokens: int = 80,
35
  backend_temperature: float = 0.8,
 
36
  ) -> TheaterSession:
37
  cleaned_premise = _clean_premise(premise)
38
  show_title = _title_from_premise(cleaned_premise)
@@ -66,10 +67,12 @@ def create_show_from_premise(
66
  ]
67
 
68
  active_backend = backend_name if backend_name in {"deterministic", "openbmb"} else "deterministic"
 
69
  model_note = f" ({backend_model_id})" if backend_model_id else ""
70
  director_log = [
71
  "Director created a deterministic six-beat show plan.",
72
  f"Active backend: {active_backend}{model_note}.",
 
73
  f"Setting selected: {setting}.",
74
  "Three puppet actors are waiting for the first beat.",
75
  ]
@@ -78,6 +81,7 @@ def create_show_from_premise(
78
  "actors_created:3",
79
  "director_plan_created",
80
  f"backend_active:{active_backend}",
 
81
  ]
82
 
83
  return TheaterSession(
@@ -98,5 +102,6 @@ def create_show_from_premise(
98
  backend_model_id=backend_model_id,
99
  backend_max_new_tokens=backend_max_new_tokens,
100
  backend_temperature=backend_temperature,
 
101
  play_opening_curtain=True,
102
  )
 
33
  backend_model_id: str | None = None,
34
  backend_max_new_tokens: int = 80,
35
  backend_temperature: float = 0.8,
36
+ director_mode: str = "deterministic",
37
  ) -> TheaterSession:
38
  cleaned_premise = _clean_premise(premise)
39
  show_title = _title_from_premise(cleaned_premise)
 
67
  ]
68
 
69
  active_backend = backend_name if backend_name in {"deterministic", "openbmb"} else "deterministic"
70
+ active_director_mode = director_mode if director_mode in {"deterministic", "openbmb"} else "deterministic"
71
  model_note = f" ({backend_model_id})" if backend_model_id else ""
72
  director_log = [
73
  "Director created a deterministic six-beat show plan.",
74
  f"Active backend: {active_backend}{model_note}.",
75
+ f"Director mode: {active_director_mode}.",
76
  f"Setting selected: {setting}.",
77
  "Three puppet actors are waiting for the first beat.",
78
  ]
 
81
  "actors_created:3",
82
  "director_plan_created",
83
  f"backend_active:{active_backend}",
84
+ f"director_mode_active:{active_director_mode}",
85
  ]
86
 
87
  return TheaterSession(
 
102
  backend_model_id=backend_model_id,
103
  backend_max_new_tokens=backend_max_new_tokens,
104
  backend_temperature=backend_temperature,
105
+ director_mode=active_director_mode,
106
  play_opening_curtain=True,
107
  )