Emperor555 Claude commited on
Commit
f2c5e84
·
1 Parent(s): 8a930bf

Switch to explainor-v3 with direct ASGI app return

Browse files

Try simpler ASGI approach returning demo.app directly
with queue enabled for concurrency handling.

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