Emperor555 Claude commited on
Commit
2021484
·
1 Parent(s): b7e3c16

Try explainor-v5 with web_server and script file approach

Browse files

Write gradio script to file and run as subprocess to
avoid ASGI/SSE streaming issues with Modal proxy.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

Files changed (1) hide show
  1. modal_app.py +134 -121
modal_app.py CHANGED
@@ -7,7 +7,7 @@ Run locally: modal serve modal_app.py
7
  import modal
8
 
9
  # Define the Modal app
10
- app = modal.App("explainor-v4")
11
 
12
  # Create image with dependencies
13
  image = (
@@ -18,6 +18,7 @@ image = (
18
  "httpx>=0.25.0",
19
  "python-dotenv>=1.0.0",
20
  "fastapi",
 
21
  )
22
  .add_local_dir("src", remote_path="/app/src", copy=True)
23
  )
@@ -32,127 +33,139 @@ image = (
32
  timeout=600,
33
  scaledown_window=300,
34
  )
35
- @modal.asgi_app()
36
  def serve():
37
- """Serve the Gradio app."""
 
38
  import sys
 
 
 
39
  sys.path.insert(0, "/app")
40
 
41
- import os
42
- import tempfile
43
- import gradio as gr
44
- from fastapi import FastAPI
45
- from src.personas import get_persona_names, get_persona
46
- from src.agent import run_agent
47
- from src.tts import generate_speech
48
-
49
- def format_sources(sources):
50
- if not sources:
51
- return "*No external sources used*"
52
- md = ""
53
- for i, src in enumerate(sources, 1):
54
- if src.get("url"):
55
- md += f"{i}. [{src['title']}]({src['url']})\n"
56
- else:
57
- md += f"{i}. {src['title']} ({src.get('source', 'General')})\n"
58
- return md
59
-
60
- def format_mcp_tools(tools):
61
- if not tools:
62
- return "*No tools used*"
63
- md = "**Agent Tool Calls:**\n\n"
64
- for tool in tools:
65
- md += f"| {tool['icon']} | `{tool['name']}` | {tool['desc']} |\n"
66
- return md
67
-
68
- def explain_topic(topic, persona_name, audience=""):
69
- if not topic.strip():
70
- return "Please enter a topic!", "", "", ""
71
- if not persona_name:
72
- persona_name = "5-Year-Old"
73
-
74
- # Check API key
75
- nebius_key = os.getenv("NEBIUS_API_KEY")
76
- if not nebius_key:
77
- available_keys = [k for k in os.environ.keys() if 'KEY' in k or 'API' in k or 'NEBIUS' in k]
78
- return f"Error: NEBIUS_API_KEY not found. Available: {available_keys}", "", "", ""
79
-
80
- steps_log = []
81
- explanation = ""
82
- sources = []
83
- mcp_tools = []
84
- try:
85
- for update in run_agent(topic, persona_name, audience):
86
- if update["type"] == "step":
87
- steps_log.append(f"**{update['title']}**\n{update['content']}")
88
- if update["step"] == "research_done" and "sources" in update:
89
- sources = update["sources"]
90
- elif update["type"] == "result":
91
- explanation = update["explanation"]
92
- sources = update.get("sources", sources)
93
- mcp_tools = update.get("mcp_tools", [])
94
- except Exception as e:
95
- import traceback
96
- return f"Error: {str(e)}\n\n{traceback.format_exc()}", "", "\n\n---\n\n".join(steps_log), ""
97
- return explanation, format_sources(sources), "\n\n---\n\n".join(steps_log), format_mcp_tools(mcp_tools)
98
-
99
- def generate_audio(explanation, persona_name):
100
- if not explanation or not explanation.strip():
101
- return None
102
- if not persona_name:
103
- persona_name = "5-Year-Old"
104
- try:
105
- persona = get_persona(persona_name)
106
- audio_bytes = generate_speech(explanation, persona["voice_id"], persona.get("voice_settings"))
107
- with tempfile.NamedTemporaryFile(suffix=".mp3", delete=False) as f:
108
- f.write(audio_bytes)
109
- return f.name
110
- except Exception as e:
111
- raise gr.Error(f"Audio generation failed: {str(e)}")
112
-
113
- # Get persona names as a static list
114
- persona_names = list(get_persona_names())
115
-
116
- with gr.Blocks(title="Explainor") as demo:
117
- gr.Markdown("# Explainor\n### Learn anything through the voice of your favorite characters!")
118
- with gr.Row():
119
- topic_input = gr.Textbox(label="Topic", placeholder="e.g., Quantum Computing")
120
- persona_dropdown = gr.Dropdown(choices=persona_names, value="5-Year-Old", label="Persona")
121
- audience_dropdown = gr.Dropdown(
122
- choices=["Just me", "Confused grandmother", "Skeptical robot", "Alien"],
123
- value="Just me",
124
- label="Audience"
125
- )
126
- explain_btn = gr.Button("Explain!", variant="primary")
127
- explanation_output = gr.Textbox(label="Explanation", lines=6)
128
- read_aloud_btn = gr.Button("Read Aloud")
129
- audio_output = gr.Audio(label="Listen", type="filepath", autoplay=True)
130
- with gr.Accordion("Tools", open=False):
131
- mcp_output = gr.Markdown("")
132
- with gr.Accordion("Sources", open=False):
133
- sources_output = gr.Markdown("")
134
- with gr.Accordion("Trace", open=False):
135
- steps_output = gr.Markdown("")
136
-
137
- def do_explain(topic, persona, audience):
138
- aud = "" if "Just me" in audience else audience
139
- return explain_topic(topic, persona, aud)
140
-
141
- explain_btn.click(
142
- fn=do_explain,
143
- inputs=[topic_input, persona_dropdown, audience_dropdown],
144
- outputs=[explanation_output, sources_output, steps_output, mcp_output],
145
- )
146
- read_aloud_btn.click(
147
- fn=generate_audio,
148
- inputs=[explanation_output, persona_dropdown],
149
- outputs=[audio_output],
150
- )
151
-
152
- # Create FastAPI app and mount Gradio
153
- fastapi_app = FastAPI()
154
-
155
- # Mount Gradio app
156
- fastapi_app = gr.mount_gradio_app(fastapi_app, demo, path="/")
157
-
158
- return fastapi_app
 
 
 
 
 
 
 
 
 
7
  import modal
8
 
9
  # Define the Modal app
10
+ app = modal.App("explainor-v5")
11
 
12
  # Create image with dependencies
13
  image = (
 
18
  "httpx>=0.25.0",
19
  "python-dotenv>=1.0.0",
20
  "fastapi",
21
+ "uvicorn",
22
  )
23
  .add_local_dir("src", remote_path="/app/src", copy=True)
24
  )
 
33
  timeout=600,
34
  scaledown_window=300,
35
  )
36
+ @modal.web_server(port=7860, startup_timeout=120)
37
  def serve():
38
+ """Serve the Gradio app via web_server."""
39
+ import subprocess
40
  import sys
41
+ import os
42
+
43
+ os.chdir("/app")
44
  sys.path.insert(0, "/app")
45
 
46
+ # Write a standalone gradio script
47
+ script = '''
48
+ import sys
49
+ sys.path.insert(0, "/app")
50
+
51
+ import os
52
+ import tempfile
53
+ import gradio as gr
54
+ from src.personas import get_persona_names, get_persona
55
+ from src.agent import run_agent
56
+ from src.tts import generate_speech
57
+
58
+ def format_sources(sources):
59
+ if not sources:
60
+ return "*No external sources used*"
61
+ md = ""
62
+ for i, src in enumerate(sources, 1):
63
+ if src.get("url"):
64
+ md += f"{i}. [{src['title']}]({src['url']})\\n"
65
+ else:
66
+ md += f"{i}. {src['title']} ({src.get('source', 'General')})\\n"
67
+ return md
68
+
69
+ def format_mcp_tools(tools):
70
+ if not tools:
71
+ return "*No tools used*"
72
+ md = "**Agent Tool Calls:**\\n\\n"
73
+ for tool in tools:
74
+ md += f"| {tool['icon']} | `{tool['name']}` | {tool['desc']} |\\n"
75
+ return md
76
+
77
+ def explain_topic(topic, persona_name, audience=""):
78
+ import traceback
79
+ if not topic.strip():
80
+ return "Please enter a topic!", "", "", ""
81
+ if not persona_name:
82
+ persona_name = "5-Year-Old"
83
+
84
+ # Check API key
85
+ nebius_key = os.getenv("NEBIUS_API_KEY")
86
+ if not nebius_key:
87
+ available_keys = [k for k in os.environ.keys() if "KEY" in k or "API" in k or "NEBIUS" in k]
88
+ return f"Error: NEBIUS_API_KEY not found. Available: {available_keys}", "", "", ""
89
+
90
+ steps_log = []
91
+ explanation = ""
92
+ sources = []
93
+ mcp_tools = []
94
+ try:
95
+ for update in run_agent(topic, persona_name, audience):
96
+ if update["type"] == "step":
97
+ steps_log.append(f"**{update['title']}**\\n{update['content']}")
98
+ if update["step"] == "research_done" and "sources" in update:
99
+ sources = update["sources"]
100
+ elif update["type"] == "result":
101
+ explanation = update["explanation"]
102
+ sources = update.get("sources", sources)
103
+ mcp_tools = update.get("mcp_tools", [])
104
+ except Exception as e:
105
+ return f"Error: {str(e)}\\n\\n{traceback.format_exc()}", "", "\\n\\n---\\n\\n".join(steps_log), ""
106
+ return explanation, format_sources(sources), "\\n\\n---\\n\\n".join(steps_log), format_mcp_tools(mcp_tools)
107
+
108
+ def generate_audio(explanation, persona_name):
109
+ if not explanation or not explanation.strip():
110
+ return None
111
+ if not persona_name:
112
+ persona_name = "5-Year-Old"
113
+ try:
114
+ persona = get_persona(persona_name)
115
+ audio_bytes = generate_speech(explanation, persona["voice_id"], persona.get("voice_settings"))
116
+ with tempfile.NamedTemporaryFile(suffix=".mp3", delete=False) as f:
117
+ f.write(audio_bytes)
118
+ return f.name
119
+ except Exception as e:
120
+ raise gr.Error(f"Audio generation failed: {str(e)}")
121
+
122
+ # Get persona names as a static list
123
+ persona_names = list(get_persona_names())
124
+
125
+ with gr.Blocks(title="Explainor") as demo:
126
+ gr.Markdown("# Explainor\\n### Learn anything through the voice of your favorite characters!")
127
+ with gr.Row():
128
+ topic_input = gr.Textbox(label="Topic", placeholder="e.g., Quantum Computing")
129
+ persona_dropdown = gr.Dropdown(choices=persona_names, value="5-Year-Old", label="Persona")
130
+ audience_dropdown = gr.Dropdown(
131
+ choices=["Just me", "Confused grandmother", "Skeptical robot", "Alien"],
132
+ value="Just me",
133
+ label="Audience"
134
+ )
135
+ explain_btn = gr.Button("Explain!", variant="primary")
136
+ explanation_output = gr.Textbox(label="Explanation", lines=6)
137
+ read_aloud_btn = gr.Button("Read Aloud")
138
+ audio_output = gr.Audio(label="Listen", type="filepath", autoplay=True)
139
+ with gr.Accordion("Tools", open=False):
140
+ mcp_output = gr.Markdown("")
141
+ with gr.Accordion("Sources", open=False):
142
+ sources_output = gr.Markdown("")
143
+ with gr.Accordion("Trace", open=False):
144
+ steps_output = gr.Markdown("")
145
+
146
+ def do_explain(topic, persona, audience):
147
+ aud = "" if "Just me" in audience else audience
148
+ return explain_topic(topic, persona, aud)
149
+
150
+ explain_btn.click(
151
+ fn=do_explain,
152
+ inputs=[topic_input, persona_dropdown, audience_dropdown],
153
+ outputs=[explanation_output, sources_output, steps_output, mcp_output],
154
+ )
155
+ read_aloud_btn.click(
156
+ fn=generate_audio,
157
+ inputs=[explanation_output, persona_dropdown],
158
+ outputs=[audio_output],
159
+ )
160
+
161
+ if __name__ == "__main__":
162
+ demo.launch(server_name="0.0.0.0", server_port=7860, share=False)
163
+ '''
164
+
165
+ # Write to file and run
166
+ with open("/app/run_gradio.py", "w") as f:
167
+ f.write(script)
168
+
169
+ # Run the script with environment variables
170
+ env = os.environ.copy()
171
+ subprocess.Popen([sys.executable, "/app/run_gradio.py"], env=env)