Commit ·
67fbecc
1
Parent(s): 017769b
feat: add trace export
Browse files- README.md +6 -0
- app.py +101 -15
- puppet_theater/actions.py +29 -4
- puppet_theater/director.py +67 -48
- puppet_theater/models.py +6 -2
- puppet_theater/session.py +21 -10
- puppet_theater/trace.py +144 -0
README.md
CHANGED
|
@@ -33,3 +33,9 @@ The default model string includes the provider suffix used by Hugging Face Infer
|
|
| 33 |
Configure the token as a Hugging Face Space secret named `HF_TOKEN`. `HUGGINGFACEHUB_API_TOKEN` is also supported. Do not commit tokens to the repository, README, app code, logs, or trace output.
|
| 34 |
|
| 35 |
Local deterministic mode and app launch work without `HF_TOKEN`.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
Configure the token as a Hugging Face Space secret named `HF_TOKEN`. `HUGGINGFACEHUB_API_TOKEN` is also supported. Do not commit tokens to the repository, README, app code, logs, or trace output.
|
| 34 |
|
| 35 |
Local deterministic mode and app launch work without `HF_TOKEN`.
|
| 36 |
+
|
| 37 |
+
## Trace export
|
| 38 |
+
|
| 39 |
+
Use the collapsed **Trace / Debug** panel to inspect a readable JSON preview of the current show trace. After creating a show, the **Download Trace JSON** button saves a file named `ai-puppet-theater-trace-<session_id>.json`.
|
| 40 |
+
|
| 41 |
+
The trace includes session metadata, the premise, title, setting, selected actor and Director backends, model ids when used, Director decisions, actor responses, audience actions, validation status, latency, and fallback metadata. Trace output is sanitized for sharing: it does not include Hugging Face tokens, environment variable values, private local paths, raw tracebacks, actor secrets, or hidden reasoning.
|
app.py
CHANGED
|
@@ -16,6 +16,7 @@ from puppet_theater import (
|
|
| 16 |
throw_prop,
|
| 17 |
warm_up_openbmb,
|
| 18 |
)
|
|
|
|
| 19 |
|
| 20 |
|
| 21 |
EMPTY_STAGE = """
|
|
@@ -1598,9 +1599,7 @@ def render_director_log(session: TheaterSession | None) -> str:
|
|
| 1598 |
|
| 1599 |
|
| 1600 |
def render_trace(session: TheaterSession | None) -> str:
|
| 1601 |
-
|
| 1602 |
-
return EMPTY_TRACE
|
| 1603 |
-
return "\n".join(f"- {entry}" for entry in session.trace_events)
|
| 1604 |
|
| 1605 |
|
| 1606 |
def normalize_backend_name(backend_name: str | None) -> str:
|
|
@@ -1700,6 +1699,7 @@ def render_outputs(session: TheaterSession | None):
|
|
| 1700 |
render_transcript(session),
|
| 1701 |
render_director_log(session),
|
| 1702 |
render_trace(session),
|
|
|
|
| 1703 |
render_backend_settings(session),
|
| 1704 |
)
|
| 1705 |
|
|
@@ -1724,6 +1724,7 @@ def create_show(
|
|
| 1724 |
"No premise yet. Add a premise to raise the curtain.",
|
| 1725 |
EMPTY_DIRECTOR_LOG,
|
| 1726 |
EMPTY_TRACE,
|
|
|
|
| 1727 |
render_backend_settings(
|
| 1728 |
None,
|
| 1729 |
selected_backend,
|
|
@@ -1760,6 +1761,7 @@ def reset_show():
|
|
| 1760 |
EMPTY_TRANSCRIPT,
|
| 1761 |
EMPTY_DIRECTOR_LOG,
|
| 1762 |
EMPTY_TRACE,
|
|
|
|
| 1763 |
"deterministic",
|
| 1764 |
"deterministic",
|
| 1765 |
DEFAULT_MAX_NEW_TOKENS,
|
|
@@ -1783,6 +1785,7 @@ def advance_one_beat(
|
|
| 1783 |
"Create a show before running a beat.",
|
| 1784 |
EMPTY_DIRECTOR_LOG,
|
| 1785 |
EMPTY_TRACE,
|
|
|
|
| 1786 |
render_backend_settings(None, backend_name, director_mode, max_new_tokens, temperature),
|
| 1787 |
)
|
| 1788 |
|
|
@@ -1806,6 +1809,7 @@ def advance_full_act(
|
|
| 1806 |
"Create a show before running the full act.",
|
| 1807 |
EMPTY_DIRECTOR_LOG,
|
| 1808 |
EMPTY_TRACE,
|
|
|
|
| 1809 |
render_backend_settings(None, backend_name, director_mode, max_new_tokens, temperature),
|
| 1810 |
)
|
| 1811 |
return
|
|
@@ -1821,7 +1825,14 @@ def advance_full_act(
|
|
| 1821 |
session.director_log.append(
|
| 1822 |
"OpenBMB is selected, so Run Full Act will use deterministic playback for this run."
|
| 1823 |
)
|
| 1824 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1825 |
|
| 1826 |
if session.beat_index >= session.max_beats:
|
| 1827 |
if deterministic_full_act:
|
|
@@ -1862,6 +1873,7 @@ def throw_audience_prop(
|
|
| 1862 |
"Create a show before throwing a prop.",
|
| 1863 |
EMPTY_DIRECTOR_LOG,
|
| 1864 |
EMPTY_TRACE,
|
|
|
|
| 1865 |
render_backend_settings(None, backend_name, director_mode, max_new_tokens, temperature),
|
| 1866 |
)
|
| 1867 |
|
|
@@ -1885,6 +1897,7 @@ def summon_audience_actor(
|
|
| 1885 |
"Create a show before summoning an actor.",
|
| 1886 |
EMPTY_DIRECTOR_LOG,
|
| 1887 |
EMPTY_TRACE,
|
|
|
|
| 1888 |
render_backend_settings(None, backend_name, director_mode, max_new_tokens, temperature),
|
| 1889 |
)
|
| 1890 |
|
|
@@ -1907,6 +1920,7 @@ def request_audience_finale(
|
|
| 1907 |
"Create a show before requesting a finale.",
|
| 1908 |
EMPTY_DIRECTOR_LOG,
|
| 1909 |
EMPTY_TRACE,
|
|
|
|
| 1910 |
render_backend_settings(None, backend_name, director_mode, max_new_tokens, temperature),
|
| 1911 |
)
|
| 1912 |
|
|
@@ -1931,15 +1945,33 @@ def warm_up_backend(
|
|
| 1931 |
session.backend_temperature = selected_temperature
|
| 1932 |
if status.load_status == "loaded":
|
| 1933 |
session.director_log.append(f"OpenBMB warm-up loaded {status.model_id}.")
|
| 1934 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1935 |
else:
|
| 1936 |
reason = status.latest_fallback_reason or "unknown error"
|
| 1937 |
session.director_log.append(f"OpenBMB warm-up failed: {reason}.")
|
| 1938 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1939 |
return (
|
| 1940 |
session,
|
| 1941 |
render_director_log(session),
|
| 1942 |
render_trace(session),
|
|
|
|
| 1943 |
render_backend_settings(
|
| 1944 |
session,
|
| 1945 |
"openbmb",
|
|
@@ -2030,11 +2062,16 @@ with gr.Blocks(title="AI Puppet Theater") as app:
|
|
| 2030 |
with gr.Accordion("Trace / Debug", open=False):
|
| 2031 |
trace_output = gr.Textbox(
|
| 2032 |
value=EMPTY_TRACE,
|
| 2033 |
-
label="Trace
|
| 2034 |
-
lines=
|
| 2035 |
interactive=False,
|
| 2036 |
elem_classes=["no-field-label"],
|
| 2037 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2038 |
with gr.Accordion("Backend", open=False):
|
| 2039 |
backend_select = gr.Dropdown(
|
| 2040 |
choices=BACKEND_CHOICES,
|
|
@@ -2089,7 +2126,15 @@ with gr.Blocks(title="AI Puppet Theater") as app:
|
|
| 2089 |
max_new_tokens_input,
|
| 2090 |
temperature_input,
|
| 2091 |
],
|
| 2092 |
-
outputs=[
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2093 |
)
|
| 2094 |
run_one_button.click(
|
| 2095 |
advance_one_beat,
|
|
@@ -2100,7 +2145,15 @@ with gr.Blocks(title="AI Puppet Theater") as app:
|
|
| 2100 |
max_new_tokens_input,
|
| 2101 |
temperature_input,
|
| 2102 |
],
|
| 2103 |
-
outputs=[
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2104 |
)
|
| 2105 |
run_full_button.click(
|
| 2106 |
advance_full_act,
|
|
@@ -2112,7 +2165,15 @@ with gr.Blocks(title="AI Puppet Theater") as app:
|
|
| 2112 |
temperature_input,
|
| 2113 |
deterministic_full_act_input,
|
| 2114 |
],
|
| 2115 |
-
outputs=[
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2116 |
)
|
| 2117 |
throw_prop_button.click(
|
| 2118 |
throw_audience_prop,
|
|
@@ -2124,7 +2185,15 @@ with gr.Blocks(title="AI Puppet Theater") as app:
|
|
| 2124 |
max_new_tokens_input,
|
| 2125 |
temperature_input,
|
| 2126 |
],
|
| 2127 |
-
outputs=[
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2128 |
)
|
| 2129 |
summon_actor_button.click(
|
| 2130 |
summon_audience_actor,
|
|
@@ -2136,7 +2205,15 @@ with gr.Blocks(title="AI Puppet Theater") as app:
|
|
| 2136 |
max_new_tokens_input,
|
| 2137 |
temperature_input,
|
| 2138 |
],
|
| 2139 |
-
outputs=[
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2140 |
)
|
| 2141 |
request_finale_button.click(
|
| 2142 |
request_audience_finale,
|
|
@@ -2147,12 +2224,20 @@ with gr.Blocks(title="AI Puppet Theater") as app:
|
|
| 2147 |
max_new_tokens_input,
|
| 2148 |
temperature_input,
|
| 2149 |
],
|
| 2150 |
-
outputs=[
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2151 |
)
|
| 2152 |
warm_up_button.click(
|
| 2153 |
warm_up_backend,
|
| 2154 |
inputs=[session_state, max_new_tokens_input, temperature_input],
|
| 2155 |
-
outputs=[session_state, director_output, trace_output, backend_output],
|
| 2156 |
)
|
| 2157 |
reset_button.click(
|
| 2158 |
reset_show,
|
|
@@ -2165,6 +2250,7 @@ with gr.Blocks(title="AI Puppet Theater") as app:
|
|
| 2165 |
transcript_output,
|
| 2166 |
director_output,
|
| 2167 |
trace_output,
|
|
|
|
| 2168 |
backend_select,
|
| 2169 |
director_mode_select,
|
| 2170 |
max_new_tokens_input,
|
|
|
|
| 16 |
throw_prop,
|
| 17 |
warm_up_openbmb,
|
| 18 |
)
|
| 19 |
+
from puppet_theater.trace import add_trace_event, render_trace_json, write_trace_json_file
|
| 20 |
|
| 21 |
|
| 22 |
EMPTY_STAGE = """
|
|
|
|
| 1599 |
|
| 1600 |
|
| 1601 |
def render_trace(session: TheaterSession | None) -> str:
|
| 1602 |
+
return render_trace_json(session)
|
|
|
|
|
|
|
| 1603 |
|
| 1604 |
|
| 1605 |
def normalize_backend_name(backend_name: str | None) -> str:
|
|
|
|
| 1699 |
render_transcript(session),
|
| 1700 |
render_director_log(session),
|
| 1701 |
render_trace(session),
|
| 1702 |
+
write_trace_json_file(session),
|
| 1703 |
render_backend_settings(session),
|
| 1704 |
)
|
| 1705 |
|
|
|
|
| 1724 |
"No premise yet. Add a premise to raise the curtain.",
|
| 1725 |
EMPTY_DIRECTOR_LOG,
|
| 1726 |
EMPTY_TRACE,
|
| 1727 |
+
None,
|
| 1728 |
render_backend_settings(
|
| 1729 |
None,
|
| 1730 |
selected_backend,
|
|
|
|
| 1761 |
EMPTY_TRANSCRIPT,
|
| 1762 |
EMPTY_DIRECTOR_LOG,
|
| 1763 |
EMPTY_TRACE,
|
| 1764 |
+
None,
|
| 1765 |
"deterministic",
|
| 1766 |
"deterministic",
|
| 1767 |
DEFAULT_MAX_NEW_TOKENS,
|
|
|
|
| 1785 |
"Create a show before running a beat.",
|
| 1786 |
EMPTY_DIRECTOR_LOG,
|
| 1787 |
EMPTY_TRACE,
|
| 1788 |
+
None,
|
| 1789 |
render_backend_settings(None, backend_name, director_mode, max_new_tokens, temperature),
|
| 1790 |
)
|
| 1791 |
|
|
|
|
| 1809 |
"Create a show before running the full act.",
|
| 1810 |
EMPTY_DIRECTOR_LOG,
|
| 1811 |
EMPTY_TRACE,
|
| 1812 |
+
None,
|
| 1813 |
render_backend_settings(None, backend_name, director_mode, max_new_tokens, temperature),
|
| 1814 |
)
|
| 1815 |
return
|
|
|
|
| 1825 |
session.director_log.append(
|
| 1826 |
"OpenBMB is selected, so Run Full Act will use deterministic playback for this run."
|
| 1827 |
)
|
| 1828 |
+
add_trace_event(
|
| 1829 |
+
session,
|
| 1830 |
+
"full_act_deterministic_playback",
|
| 1831 |
+
backend_name=selected_backend,
|
| 1832 |
+
director_mode=selected_director_mode,
|
| 1833 |
+
fallback_used=True,
|
| 1834 |
+
fallback_reason="OpenBMB full-act playback uses deterministic mode to keep the demo responsive.",
|
| 1835 |
+
)
|
| 1836 |
|
| 1837 |
if session.beat_index >= session.max_beats:
|
| 1838 |
if deterministic_full_act:
|
|
|
|
| 1873 |
"Create a show before throwing a prop.",
|
| 1874 |
EMPTY_DIRECTOR_LOG,
|
| 1875 |
EMPTY_TRACE,
|
| 1876 |
+
None,
|
| 1877 |
render_backend_settings(None, backend_name, director_mode, max_new_tokens, temperature),
|
| 1878 |
)
|
| 1879 |
|
|
|
|
| 1897 |
"Create a show before summoning an actor.",
|
| 1898 |
EMPTY_DIRECTOR_LOG,
|
| 1899 |
EMPTY_TRACE,
|
| 1900 |
+
None,
|
| 1901 |
render_backend_settings(None, backend_name, director_mode, max_new_tokens, temperature),
|
| 1902 |
)
|
| 1903 |
|
|
|
|
| 1920 |
"Create a show before requesting a finale.",
|
| 1921 |
EMPTY_DIRECTOR_LOG,
|
| 1922 |
EMPTY_TRACE,
|
| 1923 |
+
None,
|
| 1924 |
render_backend_settings(None, backend_name, director_mode, max_new_tokens, temperature),
|
| 1925 |
)
|
| 1926 |
|
|
|
|
| 1945 |
session.backend_temperature = selected_temperature
|
| 1946 |
if status.load_status == "loaded":
|
| 1947 |
session.director_log.append(f"OpenBMB warm-up loaded {status.model_id}.")
|
| 1948 |
+
add_trace_event(
|
| 1949 |
+
session,
|
| 1950 |
+
"backend_warmup",
|
| 1951 |
+
backend_name="openbmb",
|
| 1952 |
+
model_id=status.model_id,
|
| 1953 |
+
latency_ms=status.latest_latency_ms,
|
| 1954 |
+
validation_status="loaded",
|
| 1955 |
+
fallback_used=False,
|
| 1956 |
+
)
|
| 1957 |
else:
|
| 1958 |
reason = status.latest_fallback_reason or "unknown error"
|
| 1959 |
session.director_log.append(f"OpenBMB warm-up failed: {reason}.")
|
| 1960 |
+
add_trace_event(
|
| 1961 |
+
session,
|
| 1962 |
+
"backend_warmup",
|
| 1963 |
+
backend_name="openbmb",
|
| 1964 |
+
model_id=status.model_id,
|
| 1965 |
+
latency_ms=status.latest_latency_ms,
|
| 1966 |
+
validation_status=status.load_status,
|
| 1967 |
+
fallback_used=True,
|
| 1968 |
+
fallback_reason=reason,
|
| 1969 |
+
)
|
| 1970 |
return (
|
| 1971 |
session,
|
| 1972 |
render_director_log(session),
|
| 1973 |
render_trace(session),
|
| 1974 |
+
write_trace_json_file(session),
|
| 1975 |
render_backend_settings(
|
| 1976 |
session,
|
| 1977 |
"openbmb",
|
|
|
|
| 2062 |
with gr.Accordion("Trace / Debug", open=False):
|
| 2063 |
trace_output = gr.Textbox(
|
| 2064 |
value=EMPTY_TRACE,
|
| 2065 |
+
label="Trace JSON Preview",
|
| 2066 |
+
lines=10,
|
| 2067 |
interactive=False,
|
| 2068 |
elem_classes=["no-field-label"],
|
| 2069 |
)
|
| 2070 |
+
trace_download = gr.DownloadButton(
|
| 2071 |
+
"Download Trace JSON",
|
| 2072 |
+
value=None,
|
| 2073 |
+
elem_classes=["cue-action"],
|
| 2074 |
+
)
|
| 2075 |
with gr.Accordion("Backend", open=False):
|
| 2076 |
backend_select = gr.Dropdown(
|
| 2077 |
choices=BACKEND_CHOICES,
|
|
|
|
| 2126 |
max_new_tokens_input,
|
| 2127 |
temperature_input,
|
| 2128 |
],
|
| 2129 |
+
outputs=[
|
| 2130 |
+
session_state,
|
| 2131 |
+
stage_output,
|
| 2132 |
+
transcript_output,
|
| 2133 |
+
director_output,
|
| 2134 |
+
trace_output,
|
| 2135 |
+
trace_download,
|
| 2136 |
+
backend_output,
|
| 2137 |
+
],
|
| 2138 |
)
|
| 2139 |
run_one_button.click(
|
| 2140 |
advance_one_beat,
|
|
|
|
| 2145 |
max_new_tokens_input,
|
| 2146 |
temperature_input,
|
| 2147 |
],
|
| 2148 |
+
outputs=[
|
| 2149 |
+
session_state,
|
| 2150 |
+
stage_output,
|
| 2151 |
+
transcript_output,
|
| 2152 |
+
director_output,
|
| 2153 |
+
trace_output,
|
| 2154 |
+
trace_download,
|
| 2155 |
+
backend_output,
|
| 2156 |
+
],
|
| 2157 |
)
|
| 2158 |
run_full_button.click(
|
| 2159 |
advance_full_act,
|
|
|
|
| 2165 |
temperature_input,
|
| 2166 |
deterministic_full_act_input,
|
| 2167 |
],
|
| 2168 |
+
outputs=[
|
| 2169 |
+
session_state,
|
| 2170 |
+
stage_output,
|
| 2171 |
+
transcript_output,
|
| 2172 |
+
director_output,
|
| 2173 |
+
trace_output,
|
| 2174 |
+
trace_download,
|
| 2175 |
+
backend_output,
|
| 2176 |
+
],
|
| 2177 |
)
|
| 2178 |
throw_prop_button.click(
|
| 2179 |
throw_audience_prop,
|
|
|
|
| 2185 |
max_new_tokens_input,
|
| 2186 |
temperature_input,
|
| 2187 |
],
|
| 2188 |
+
outputs=[
|
| 2189 |
+
session_state,
|
| 2190 |
+
stage_output,
|
| 2191 |
+
transcript_output,
|
| 2192 |
+
director_output,
|
| 2193 |
+
trace_output,
|
| 2194 |
+
trace_download,
|
| 2195 |
+
backend_output,
|
| 2196 |
+
],
|
| 2197 |
)
|
| 2198 |
summon_actor_button.click(
|
| 2199 |
summon_audience_actor,
|
|
|
|
| 2205 |
max_new_tokens_input,
|
| 2206 |
temperature_input,
|
| 2207 |
],
|
| 2208 |
+
outputs=[
|
| 2209 |
+
session_state,
|
| 2210 |
+
stage_output,
|
| 2211 |
+
transcript_output,
|
| 2212 |
+
director_output,
|
| 2213 |
+
trace_output,
|
| 2214 |
+
trace_download,
|
| 2215 |
+
backend_output,
|
| 2216 |
+
],
|
| 2217 |
)
|
| 2218 |
request_finale_button.click(
|
| 2219 |
request_audience_finale,
|
|
|
|
| 2224 |
max_new_tokens_input,
|
| 2225 |
temperature_input,
|
| 2226 |
],
|
| 2227 |
+
outputs=[
|
| 2228 |
+
session_state,
|
| 2229 |
+
stage_output,
|
| 2230 |
+
transcript_output,
|
| 2231 |
+
director_output,
|
| 2232 |
+
trace_output,
|
| 2233 |
+
trace_download,
|
| 2234 |
+
backend_output,
|
| 2235 |
+
],
|
| 2236 |
)
|
| 2237 |
warm_up_button.click(
|
| 2238 |
warm_up_backend,
|
| 2239 |
inputs=[session_state, max_new_tokens_input, temperature_input],
|
| 2240 |
+
outputs=[session_state, director_output, trace_output, trace_download, backend_output],
|
| 2241 |
)
|
| 2242 |
reset_button.click(
|
| 2243 |
reset_show,
|
|
|
|
| 2250 |
transcript_output,
|
| 2251 |
director_output,
|
| 2252 |
trace_output,
|
| 2253 |
+
trace_download,
|
| 2254 |
backend_select,
|
| 2255 |
director_mode_select,
|
| 2256 |
max_new_tokens_input,
|
puppet_theater/actions.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
| 1 |
from puppet_theater.models import Actor, TheaterSession
|
|
|
|
| 2 |
|
| 3 |
|
| 4 |
MAX_ACTORS = 4
|
|
@@ -13,7 +14,13 @@ def throw_prop(session: TheaterSession | None, prop_name: str) -> TheaterSession
|
|
| 13 |
session.latest_prop = prop
|
| 14 |
session.latest_audience_action = f"Audience threw {prop} onto the stage."
|
| 15 |
session.director_log.append(session.latest_audience_action)
|
| 16 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
return session
|
| 18 |
|
| 19 |
|
|
@@ -25,7 +32,14 @@ def summon_actor(session: TheaterSession | None, actor_name: str) -> TheaterSess
|
|
| 25 |
if len(session.actors) >= MAX_ACTORS:
|
| 26 |
session.latest_audience_action = "Audience tried to summon an actor, but the stage is full."
|
| 27 |
session.director_log.append(session.latest_audience_action)
|
| 28 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
return session
|
| 30 |
|
| 31 |
actor = Actor(
|
|
@@ -39,7 +53,13 @@ def summon_actor(session: TheaterSession | None, actor_name: str) -> TheaterSess
|
|
| 39 |
session.actors.append(actor)
|
| 40 |
session.latest_audience_action = f"Audience summoned {name}."
|
| 41 |
session.director_log.append(session.latest_audience_action)
|
| 42 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
return session
|
| 44 |
|
| 45 |
|
|
@@ -50,5 +70,10 @@ def request_finale(session: TheaterSession | None) -> TheaterSession | None:
|
|
| 50 |
session.finale_requested = True
|
| 51 |
session.latest_audience_action = "Audience requested the finale."
|
| 52 |
session.director_log.append(session.latest_audience_action)
|
| 53 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
return session
|
|
|
|
| 1 |
from puppet_theater.models import Actor, TheaterSession
|
| 2 |
+
from puppet_theater.trace import add_trace_event
|
| 3 |
|
| 4 |
|
| 5 |
MAX_ACTORS = 4
|
|
|
|
| 14 |
session.latest_prop = prop
|
| 15 |
session.latest_audience_action = f"Audience threw {prop} onto the stage."
|
| 16 |
session.director_log.append(session.latest_audience_action)
|
| 17 |
+
add_trace_event(
|
| 18 |
+
session,
|
| 19 |
+
"audience_action",
|
| 20 |
+
audience_action="throw_prop",
|
| 21 |
+
prop=prop,
|
| 22 |
+
action_summary=session.latest_audience_action,
|
| 23 |
+
)
|
| 24 |
return session
|
| 25 |
|
| 26 |
|
|
|
|
| 32 |
if len(session.actors) >= MAX_ACTORS:
|
| 33 |
session.latest_audience_action = "Audience tried to summon an actor, but the stage is full."
|
| 34 |
session.director_log.append(session.latest_audience_action)
|
| 35 |
+
add_trace_event(
|
| 36 |
+
session,
|
| 37 |
+
"audience_action",
|
| 38 |
+
audience_action="summon_actor",
|
| 39 |
+
validation_status="skipped_stage_full",
|
| 40 |
+
fallback_used=False,
|
| 41 |
+
action_summary=session.latest_audience_action,
|
| 42 |
+
)
|
| 43 |
return session
|
| 44 |
|
| 45 |
actor = Actor(
|
|
|
|
| 53 |
session.actors.append(actor)
|
| 54 |
session.latest_audience_action = f"Audience summoned {name}."
|
| 55 |
session.director_log.append(session.latest_audience_action)
|
| 56 |
+
add_trace_event(
|
| 57 |
+
session,
|
| 58 |
+
"audience_action",
|
| 59 |
+
audience_action="summon_actor",
|
| 60 |
+
summoned_actor=name,
|
| 61 |
+
action_summary=session.latest_audience_action,
|
| 62 |
+
)
|
| 63 |
return session
|
| 64 |
|
| 65 |
|
|
|
|
| 70 |
session.finale_requested = True
|
| 71 |
session.latest_audience_action = "Audience requested the finale."
|
| 72 |
session.director_log.append(session.latest_audience_action)
|
| 73 |
+
add_trace_event(
|
| 74 |
+
session,
|
| 75 |
+
"audience_action",
|
| 76 |
+
audience_action="request_finale",
|
| 77 |
+
action_summary=session.latest_audience_action,
|
| 78 |
+
)
|
| 79 |
return session
|
puppet_theater/director.py
CHANGED
|
@@ -17,6 +17,7 @@ from puppet_theater.backends import (
|
|
| 17 |
)
|
| 18 |
from puppet_theater.models import Actor, Beat, DirectorDecision, TheaterSession
|
| 19 |
from puppet_theater.prompts import DIRECTOR_DECISION_PROMPT
|
|
|
|
| 20 |
|
| 21 |
|
| 22 |
BEAT_ARC = [
|
|
@@ -344,7 +345,12 @@ def run_one_beat(session: TheaterSession | None) -> TheaterSession | None:
|
|
| 344 |
|
| 345 |
if session.beat_index >= session.max_beats:
|
| 346 |
session.director_log.append("Curtain already fallen; no new beat added.")
|
| 347 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 348 |
return session
|
| 349 |
|
| 350 |
director_generation = choose_director_decision(session)
|
|
@@ -368,29 +374,31 @@ def run_one_beat(session: TheaterSession | None) -> TheaterSession | None:
|
|
| 368 |
session.director_log.append(f"Director model id: {director_generation.model_id}.")
|
| 369 |
if director_generation.error:
|
| 370 |
session.director_log.append(f"Director fallback reason: {director_generation.error}.")
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
|
|
|
|
|
|
| 384 |
)
|
| 385 |
-
if director_generation.fallback_used:
|
| 386 |
-
session.trace_events.append(f"director_fallback_used:{director_generation.validation_status}")
|
| 387 |
if director_generation.error:
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
|
|
|
|
|
|
| 394 |
)
|
| 395 |
|
| 396 |
backend_generation = generate_actor_response(session, decision, speaker, prop)
|
|
@@ -410,7 +418,7 @@ def run_one_beat(session: TheaterSession | None) -> TheaterSession | None:
|
|
| 410 |
if prop is not None:
|
| 411 |
session.latest_prop = None
|
| 412 |
session.director_log.append(f"{speaker.name} picked up {prop} and used it in the scene.")
|
| 413 |
-
|
| 414 |
|
| 415 |
session.director_log.append(
|
| 416 |
f"Beat {session.beat_index}/{session.max_beats}: {decision.beat_type} assigned to {speaker.name}."
|
|
@@ -426,39 +434,50 @@ def run_one_beat(session: TheaterSession | None) -> TheaterSession | None:
|
|
| 426 |
if backend_generation.error:
|
| 427 |
session.director_log.append(f"Backend fallback reason: {backend_generation.error}.")
|
| 428 |
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
"
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 435 |
)
|
| 436 |
-
if backend_generation.fallback_used:
|
| 437 |
-
session.trace_events.append(f"fallback_used:{backend_generation.backend_name}:{backend_generation.validation_status}")
|
| 438 |
if backend_generation.error:
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 445 |
)
|
| 446 |
-
session.trace_events.append(
|
| 447 |
-
"backend_result:"
|
| 448 |
-
f"{backend_generation.backend_name}:"
|
| 449 |
-
f"model={backend_generation.model_id or 'none'}:"
|
| 450 |
-
f"load_status={backend_generation.load_status}:"
|
| 451 |
-
f"fallback={backend_generation.fallback_used}:"
|
| 452 |
-
f"validation={backend_generation.validation_status}:"
|
| 453 |
-
f"latency_ms={backend_generation.latency_ms}"
|
| 454 |
-
)
|
| 455 |
|
| 456 |
if decision.should_end_scene or decision.beat_type == "finale":
|
| 457 |
session.beat_index = session.max_beats
|
| 458 |
session.finale_requested = True
|
| 459 |
session.director_log.append("Finale reached; curtain falls cleanly.")
|
| 460 |
-
|
| 461 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 462 |
|
| 463 |
return session
|
| 464 |
|
|
|
|
| 17 |
)
|
| 18 |
from puppet_theater.models import Actor, Beat, DirectorDecision, TheaterSession
|
| 19 |
from puppet_theater.prompts import DIRECTOR_DECISION_PROMPT
|
| 20 |
+
from puppet_theater.trace import add_trace_event
|
| 21 |
|
| 22 |
|
| 23 |
BEAT_ARC = [
|
|
|
|
| 345 |
|
| 346 |
if session.beat_index >= session.max_beats:
|
| 347 |
session.director_log.append("Curtain already fallen; no new beat added.")
|
| 348 |
+
add_trace_event(
|
| 349 |
+
session,
|
| 350 |
+
"beat_skipped",
|
| 351 |
+
validation_status="curtain_already_fallen",
|
| 352 |
+
fallback_used=False,
|
| 353 |
+
)
|
| 354 |
return session
|
| 355 |
|
| 356 |
director_generation = choose_director_decision(session)
|
|
|
|
| 374 |
session.director_log.append(f"Director model id: {director_generation.model_id}.")
|
| 375 |
if director_generation.error:
|
| 376 |
session.director_log.append(f"Director fallback reason: {director_generation.error}.")
|
| 377 |
+
add_trace_event(
|
| 378 |
+
session,
|
| 379 |
+
"director_decision",
|
| 380 |
+
speaker=speaker.name,
|
| 381 |
+
beat_type=decision.beat_type,
|
| 382 |
+
backend_name=director_generation.director_mode,
|
| 383 |
+
model_id=director_generation.model_id,
|
| 384 |
+
latency_ms=director_generation.latency_ms,
|
| 385 |
+
validation_status=director_generation.validation_status,
|
| 386 |
+
fallback_used=director_generation.fallback_used,
|
| 387 |
+
fallback_reason=director_generation.error,
|
| 388 |
+
reason_summary=director_generation.reason_summary,
|
| 389 |
+
uses_prop=decision.uses_prop,
|
| 390 |
+
reveal_secret=decision.reveal_secret,
|
| 391 |
+
should_end_scene=decision.should_end_scene,
|
| 392 |
)
|
|
|
|
|
|
|
| 393 |
if director_generation.error:
|
| 394 |
+
add_trace_event(
|
| 395 |
+
session,
|
| 396 |
+
"director_error",
|
| 397 |
+
backend_name=director_generation.director_mode,
|
| 398 |
+
model_id=director_generation.model_id,
|
| 399 |
+
validation_status=director_generation.validation_status,
|
| 400 |
+
fallback_used=director_generation.fallback_used,
|
| 401 |
+
fallback_reason=director_generation.error,
|
| 402 |
)
|
| 403 |
|
| 404 |
backend_generation = generate_actor_response(session, decision, speaker, prop)
|
|
|
|
| 418 |
if prop is not None:
|
| 419 |
session.latest_prop = None
|
| 420 |
session.director_log.append(f"{speaker.name} picked up {prop} and used it in the scene.")
|
| 421 |
+
add_trace_event(session, "prop_used", speaker=speaker.name, prop=prop)
|
| 422 |
|
| 423 |
session.director_log.append(
|
| 424 |
f"Beat {session.beat_index}/{session.max_beats}: {decision.beat_type} assigned to {speaker.name}."
|
|
|
|
| 434 |
if backend_generation.error:
|
| 435 |
session.director_log.append(f"Backend fallback reason: {backend_generation.error}.")
|
| 436 |
|
| 437 |
+
add_trace_event(
|
| 438 |
+
session,
|
| 439 |
+
"beat_added",
|
| 440 |
+
beat_index=session.beat_index,
|
| 441 |
+
beat_type=decision.beat_type,
|
| 442 |
+
speaker=speaker.name,
|
| 443 |
+
stage_effect=beat.stage_effect,
|
| 444 |
+
)
|
| 445 |
+
add_trace_event(
|
| 446 |
+
session,
|
| 447 |
+
"actor_response",
|
| 448 |
+
beat_index=session.beat_index,
|
| 449 |
+
speaker=speaker.name,
|
| 450 |
+
backend_name=backend_generation.backend_name,
|
| 451 |
+
model_id=backend_generation.model_id,
|
| 452 |
+
latency_ms=backend_generation.latency_ms,
|
| 453 |
+
validation_status=backend_generation.validation_status,
|
| 454 |
+
fallback_used=backend_generation.fallback_used,
|
| 455 |
+
fallback_reason=backend_generation.error,
|
| 456 |
+
load_status=backend_generation.load_status,
|
| 457 |
)
|
|
|
|
|
|
|
| 458 |
if backend_generation.error:
|
| 459 |
+
add_trace_event(
|
| 460 |
+
session,
|
| 461 |
+
"backend_error",
|
| 462 |
+
beat_index=session.beat_index,
|
| 463 |
+
speaker=speaker.name,
|
| 464 |
+
backend_name=backend_generation.backend_name,
|
| 465 |
+
model_id=backend_generation.model_id,
|
| 466 |
+
validation_status=backend_generation.validation_status,
|
| 467 |
+
fallback_used=backend_generation.fallback_used,
|
| 468 |
+
fallback_reason=backend_generation.error,
|
| 469 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 470 |
|
| 471 |
if decision.should_end_scene or decision.beat_type == "finale":
|
| 472 |
session.beat_index = session.max_beats
|
| 473 |
session.finale_requested = True
|
| 474 |
session.director_log.append("Finale reached; curtain falls cleanly.")
|
| 475 |
+
add_trace_event(
|
| 476 |
+
session,
|
| 477 |
+
"scene_completed",
|
| 478 |
+
beat_index=session.beat_index,
|
| 479 |
+
reason_summary="Finale reached; curtain falls cleanly.",
|
| 480 |
+
)
|
| 481 |
|
| 482 |
return session
|
| 483 |
|
puppet_theater/models.py
CHANGED
|
@@ -1,6 +1,8 @@
|
|
| 1 |
from dataclasses import dataclass, field
|
|
|
|
|
|
|
| 2 |
|
| 3 |
-
from typing import Literal
|
| 4 |
|
| 5 |
from pydantic import BaseModel, Field, field_validator
|
| 6 |
|
|
@@ -90,6 +92,8 @@ class TheaterSession:
|
|
| 90 |
premise: str
|
| 91 |
setting: str
|
| 92 |
actors: list[Actor]
|
|
|
|
|
|
|
| 93 |
beat_index: int = 0
|
| 94 |
max_beats: int = 6
|
| 95 |
transcript: list[Beat] = field(default_factory=list)
|
|
@@ -97,7 +101,7 @@ class TheaterSession:
|
|
| 97 |
latest_prop: str | None = None
|
| 98 |
latest_audience_action: str | None = None
|
| 99 |
director_log: list[str] = field(default_factory=list)
|
| 100 |
-
trace_events: list[str] = field(default_factory=list)
|
| 101 |
finale_requested: bool = False
|
| 102 |
backend_name: str = "deterministic"
|
| 103 |
backend_model_id: str | None = None
|
|
|
|
| 1 |
from dataclasses import dataclass, field
|
| 2 |
+
from datetime import datetime, timezone
|
| 3 |
+
from uuid import uuid4
|
| 4 |
|
| 5 |
+
from typing import Any, Literal
|
| 6 |
|
| 7 |
from pydantic import BaseModel, Field, field_validator
|
| 8 |
|
|
|
|
| 92 |
premise: str
|
| 93 |
setting: str
|
| 94 |
actors: list[Actor]
|
| 95 |
+
session_id: str = field(default_factory=lambda: uuid4().hex[:12])
|
| 96 |
+
created_at: str = field(default_factory=lambda: datetime.now(timezone.utc).isoformat())
|
| 97 |
beat_index: int = 0
|
| 98 |
max_beats: int = 6
|
| 99 |
transcript: list[Beat] = field(default_factory=list)
|
|
|
|
| 101 |
latest_prop: str | None = None
|
| 102 |
latest_audience_action: str | None = None
|
| 103 |
director_log: list[str] = field(default_factory=list)
|
| 104 |
+
trace_events: list[dict[str, Any] | str] = field(default_factory=list)
|
| 105 |
finale_requested: bool = False
|
| 106 |
backend_name: str = "deterministic"
|
| 107 |
backend_model_id: str | None = None
|
puppet_theater/session.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
| 1 |
from puppet_theater.models import Actor, TheaterSession
|
|
|
|
| 2 |
|
| 3 |
|
| 4 |
def _clean_premise(premise: str) -> str:
|
|
@@ -77,15 +78,7 @@ def create_show_from_premise(
|
|
| 77 |
f"Setting selected: {setting}.",
|
| 78 |
"Three puppet actors are waiting for the first beat.",
|
| 79 |
]
|
| 80 |
-
|
| 81 |
-
"show_created",
|
| 82 |
-
"actors_created:3",
|
| 83 |
-
"director_plan_created",
|
| 84 |
-
f"backend_active:{active_backend}",
|
| 85 |
-
f"director_mode_active:{active_director_mode}",
|
| 86 |
-
]
|
| 87 |
-
|
| 88 |
-
return TheaterSession(
|
| 89 |
show_title=show_title,
|
| 90 |
premise=cleaned_premise,
|
| 91 |
setting=setting,
|
|
@@ -97,7 +90,7 @@ def create_show_from_premise(
|
|
| 97 |
latest_prop=None,
|
| 98 |
latest_audience_action=None,
|
| 99 |
director_log=director_log,
|
| 100 |
-
trace_events=
|
| 101 |
finale_requested=False,
|
| 102 |
backend_name=active_backend,
|
| 103 |
backend_model_id=backend_model_id,
|
|
@@ -106,3 +99,21 @@ def create_show_from_premise(
|
|
| 106 |
director_mode=active_director_mode,
|
| 107 |
play_opening_curtain=True,
|
| 108 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
from puppet_theater.models import Actor, TheaterSession
|
| 2 |
+
from puppet_theater.trace import add_trace_event
|
| 3 |
|
| 4 |
|
| 5 |
def _clean_premise(premise: str) -> str:
|
|
|
|
| 78 |
f"Setting selected: {setting}.",
|
| 79 |
"Three puppet actors are waiting for the first beat.",
|
| 80 |
]
|
| 81 |
+
session = TheaterSession(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
show_title=show_title,
|
| 83 |
premise=cleaned_premise,
|
| 84 |
setting=setting,
|
|
|
|
| 90 |
latest_prop=None,
|
| 91 |
latest_audience_action=None,
|
| 92 |
director_log=director_log,
|
| 93 |
+
trace_events=[],
|
| 94 |
finale_requested=False,
|
| 95 |
backend_name=active_backend,
|
| 96 |
backend_model_id=backend_model_id,
|
|
|
|
| 99 |
director_mode=active_director_mode,
|
| 100 |
play_opening_curtain=True,
|
| 101 |
)
|
| 102 |
+
add_trace_event(
|
| 103 |
+
session,
|
| 104 |
+
"show_created",
|
| 105 |
+
backend_name=active_backend,
|
| 106 |
+
model_id=backend_model_id,
|
| 107 |
+
director_mode=active_director_mode,
|
| 108 |
+
actor_count=len(actors),
|
| 109 |
+
validation_status="valid",
|
| 110 |
+
fallback_used=False,
|
| 111 |
+
)
|
| 112 |
+
add_trace_event(session, "actors_created", actor_count=len(actors))
|
| 113 |
+
add_trace_event(
|
| 114 |
+
session,
|
| 115 |
+
"director_plan_created",
|
| 116 |
+
director_mode=active_director_mode,
|
| 117 |
+
reason_summary="Deterministic six-beat show plan created.",
|
| 118 |
+
)
|
| 119 |
+
return session
|
puppet_theater/trace.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from datetime import datetime, timezone
|
| 4 |
+
import json
|
| 5 |
+
import os
|
| 6 |
+
import re
|
| 7 |
+
import tempfile
|
| 8 |
+
from typing import Any
|
| 9 |
+
|
| 10 |
+
from puppet_theater.models import TheaterSession
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
APP_NAME = "AI Puppet Theater"
|
| 14 |
+
TRACE_VERSION = "1.0"
|
| 15 |
+
_MAX_TEXT_CHARS = 500
|
| 16 |
+
_PRIVATE_PATH_PATTERNS = [
|
| 17 |
+
re.compile(r"/Users/[^\s\"'<>:]+(?:/[^\s\"'<>:]+)*"),
|
| 18 |
+
re.compile(r"/private/(?:tmp|var)/[^\s\"'<>:]+(?:/[^\s\"'<>:]+)*"),
|
| 19 |
+
re.compile(r"/tmp/[^\s\"'<>:]+(?:/[^\s\"'<>:]+)*"),
|
| 20 |
+
]
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def add_trace_event(session: TheaterSession, event_type: str, **fields: Any) -> None:
|
| 24 |
+
event: dict[str, Any] = {
|
| 25 |
+
"type": sanitize_value(event_type),
|
| 26 |
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
| 27 |
+
"step": len(session.trace_events) + 1,
|
| 28 |
+
}
|
| 29 |
+
if session.beat_index is not None:
|
| 30 |
+
event["beat_index"] = session.beat_index
|
| 31 |
+
for key, value in fields.items():
|
| 32 |
+
if value is not None:
|
| 33 |
+
event[key] = sanitize_value(value)
|
| 34 |
+
session.trace_events.append(event)
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def export_trace(session: TheaterSession | None) -> dict[str, Any] | None:
|
| 38 |
+
if session is None:
|
| 39 |
+
return None
|
| 40 |
+
|
| 41 |
+
actor_model_id = session.backend_model_id
|
| 42 |
+
director_model_id = _model_id_for_mode(session.director_mode)
|
| 43 |
+
active_model_id = actor_model_id or director_model_id
|
| 44 |
+
return {
|
| 45 |
+
"app_name": APP_NAME,
|
| 46 |
+
"trace_version": TRACE_VERSION,
|
| 47 |
+
"session_id": sanitize_value(session.session_id),
|
| 48 |
+
"created_at": sanitize_value(session.created_at),
|
| 49 |
+
"premise": sanitize_value(session.premise),
|
| 50 |
+
"title": sanitize_value(session.show_title),
|
| 51 |
+
"setting": sanitize_value(session.setting),
|
| 52 |
+
"active_backend": sanitize_value(session.backend_name),
|
| 53 |
+
"actor_backend": sanitize_value(session.backend_name),
|
| 54 |
+
"director_mode": sanitize_value(session.director_mode),
|
| 55 |
+
"director_backend": sanitize_value(session.director_mode),
|
| 56 |
+
"model_id": sanitize_value(active_model_id),
|
| 57 |
+
"actor_model_id": sanitize_value(actor_model_id),
|
| 58 |
+
"director_model_id": sanitize_value(director_model_id),
|
| 59 |
+
"events": normalize_trace_events(session),
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def render_trace_json(session: TheaterSession | None) -> str:
|
| 64 |
+
payload = export_trace(session)
|
| 65 |
+
if payload is None:
|
| 66 |
+
return "No trace events yet."
|
| 67 |
+
return json.dumps(payload, indent=2, sort_keys=False)
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def write_trace_json_file(session: TheaterSession | None) -> str | None:
|
| 71 |
+
payload = export_trace(session)
|
| 72 |
+
if payload is None:
|
| 73 |
+
return None
|
| 74 |
+
|
| 75 |
+
safe_session_id = re.sub(r"[^a-zA-Z0-9_-]+", "-", str(payload["session_id"]))[:48] or "session"
|
| 76 |
+
filename = f"ai-puppet-theater-trace-{safe_session_id}.json"
|
| 77 |
+
path = os.path.join(tempfile.gettempdir(), filename)
|
| 78 |
+
with open(path, "w", encoding="utf-8") as trace_file:
|
| 79 |
+
json.dump(payload, trace_file, indent=2, ensure_ascii=False)
|
| 80 |
+
trace_file.write("\n")
|
| 81 |
+
return path
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def normalize_trace_events(session: TheaterSession) -> list[dict[str, Any]]:
|
| 85 |
+
normalized: list[dict[str, Any]] = []
|
| 86 |
+
for index, raw_event in enumerate(session.trace_events, start=1):
|
| 87 |
+
if isinstance(raw_event, dict):
|
| 88 |
+
event = {str(key): sanitize_value(value) for key, value in raw_event.items() if value is not None}
|
| 89 |
+
event.setdefault("type", "event")
|
| 90 |
+
event.setdefault("step", index)
|
| 91 |
+
normalized.append(event)
|
| 92 |
+
continue
|
| 93 |
+
|
| 94 |
+
normalized.append(
|
| 95 |
+
{
|
| 96 |
+
"type": "legacy_event",
|
| 97 |
+
"step": index,
|
| 98 |
+
"message": sanitize_value(raw_event),
|
| 99 |
+
}
|
| 100 |
+
)
|
| 101 |
+
return normalized
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def sanitize_value(value: Any) -> Any:
|
| 105 |
+
if isinstance(value, dict):
|
| 106 |
+
return {
|
| 107 |
+
str(key): sanitize_value(item)
|
| 108 |
+
for key, item in value.items()
|
| 109 |
+
if item is not None and not _is_sensitive_key(str(key))
|
| 110 |
+
}
|
| 111 |
+
if isinstance(value, list):
|
| 112 |
+
return [sanitize_value(item) for item in value]
|
| 113 |
+
if isinstance(value, tuple):
|
| 114 |
+
return [sanitize_value(item) for item in value]
|
| 115 |
+
if isinstance(value, str):
|
| 116 |
+
return _sanitize_text(value)
|
| 117 |
+
return value
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
def _sanitize_text(value: str) -> str:
|
| 121 |
+
text = " ".join(value.split())
|
| 122 |
+
if "Traceback (most recent call last)" in text:
|
| 123 |
+
text = "Traceback redacted"
|
| 124 |
+
for secret_name in ("HF_TOKEN", "HUGGINGFACEHUB_API_TOKEN"):
|
| 125 |
+
secret = os.getenv(secret_name)
|
| 126 |
+
if secret:
|
| 127 |
+
text = text.replace(secret, "[redacted]")
|
| 128 |
+
text = re.sub(r"\b(HF_TOKEN|HUGGINGFACEHUB_API_TOKEN)\s*=\s*\S+", r"\1=[redacted]", text)
|
| 129 |
+
for pattern in _PRIVATE_PATH_PATTERNS:
|
| 130 |
+
text = pattern.sub("[redacted-path]", text)
|
| 131 |
+
return text[:_MAX_TEXT_CHARS].rstrip()
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
def _is_sensitive_key(key: str) -> bool:
|
| 135 |
+
lowered = key.lower()
|
| 136 |
+
return "token" in lowered or "secret" in lowered or "password" in lowered
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
def _model_id_for_mode(mode: str | None) -> str | None:
|
| 140 |
+
if mode == "openbmb":
|
| 141 |
+
return os.getenv("OPENBMB_MODEL_ID", "openbmb/MiniCPM5-1B")
|
| 142 |
+
if mode == "hf_api":
|
| 143 |
+
return os.getenv("HF_API_MODEL_ID", "Qwen/Qwen3-4B-Instruct-2507:nscale")
|
| 144 |
+
return None
|