Spaces:
Runtime error
Runtime error
Enrique Cardoza
fix(context): prevent context window exhaustion by removing base64 audio data
58cb2c2 Download app.py from Agents-MCP-Hackathon/transcript-generator-agent: direct link, hf CLI and curl.
- Browser
- Download file 26.8 kB
-
https://huggingface.co/spaces/Agents-MCP-Hackathon/transcript-generator-agent/resolve/main/app.py
- Command line
-
hf download hf://spaces/Agents-MCP-Hackathon/transcript-generator-agent/app.py
-
curl -L -o app.py https://huggingface.co/spaces/Agents-MCP-Hackathon/transcript-generator-agent/resolve/main/app.py
26.8 kB
| import gradio as gr | |
| import anthropic | |
| import os | |
| import json | |
| import requests | |
| import base64 | |
| import tempfile | |
| import time | |
| from pathlib import Path | |
| from dotenv import load_dotenv | |
| from smolagents.mcp_client import MCPClient | |
| from gradio import ChatMessage | |
| # Load environment variables | |
| load_dotenv() | |
| # Get API keys from environment or use None as default | |
| anthropic_api_key = os.environ.get("ANTHROPIC_API_KEY") | |
| groq_api_key = os.environ.get("GROQ_API_KEY") | |
| tts_api_url_base = os.environ.get("TTS_API_URL") | |
| # Initialize Anthropic client (will be updated if user provides a key) | |
| client = anthropic.Anthropic(api_key=anthropic_api_key) if anthropic_api_key else None | |
| # MCP connection details | |
| mcp_url = "https://agents-mcp-hackathon-transcript-generator.hf.space/gradio_api/mcp/sse" | |
| # TTS API endpoint | |
| tts_api_url = tts_api_url_base+"/generate" | |
| # Global variable to store tools | |
| mcp_tools = {} | |
| tool_descriptions = {} | |
| # Initialize tools at startup | |
| def initialize_tools(): | |
| global mcp_tools, tool_descriptions | |
| try: | |
| # Connect to MCP server and fetch tools | |
| with MCPClient({"url": mcp_url, "transport": "sse"}) as mcp_client: | |
| # Store tools in the global dictionary | |
| mcp_tools = {tool.name: tool for tool in mcp_client} | |
| # Create tool descriptions for Claude | |
| for name, tool in mcp_tools.items(): | |
| description = getattr(tool, 'description', f"Tool: {name}") | |
| parameters = getattr(tool, 'parameters', {}) | |
| tool_descriptions[name] = { | |
| "name": name, | |
| "description": description, | |
| "parameters": parameters | |
| } | |
| print(f"Loaded tools: {', '.join(mcp_tools.keys())}") | |
| return True | |
| except Exception as e: | |
| print(f"Failed to initialize MCP tools: {e}") | |
| return False | |
| # Helper: Format history for Claude | |
| def format_history(history): | |
| messages = [] | |
| if not history: | |
| return messages | |
| print(f"History type: {type(history)}, length: {len(history)}") | |
| for i, item in enumerate(history): | |
| print(f"Item {i} type: {type(item)}") | |
| if isinstance(item, dict) and 'role' in item and 'content' in item: | |
| content = item["content"] | |
| # Clean base64 audio data from content before sending to Claude | |
| content = clean_base64_audio(content) | |
| messages.append({"role": item["role"], "content": content}) | |
| elif hasattr(item, 'role') and hasattr(item, 'content'): | |
| # Handle ChatMessage objects | |
| role = item.role | |
| content = item.content | |
| # Clean base64 audio data from content before sending to Claude | |
| content = clean_base64_audio(content) | |
| # Ensure valid role values for Claude API | |
| if role not in ["user", "assistant"]: | |
| role = "user" if role == "human" else "assistant" | |
| messages.append({"role": role, "content": content}) | |
| print(f"Added message with role: {role}, content: {content[:50]}...") | |
| print(f"Formatted {len(messages)} messages for Claude") | |
| return messages | |
| # Helper function to clean base64 audio data from message content | |
| def clean_base64_audio(content): | |
| if not isinstance(content, str): | |
| return content | |
| # Check if there's base64 audio data in the content | |
| if "<audio controls src=\"data:audio/wav;base64," in content: | |
| # Replace the entire audio tag and base64 data with a placeholder | |
| import re | |
| cleaned_content = re.sub( | |
| r'<audio controls src="data:audio/wav;base64,[^"]*"[^>]*></audio>', | |
| '<audio>[BASE64_AUDIO_REMOVED_TO_SAVE_CONTEXT]</audio>', | |
| content | |
| ) | |
| # Log the size reduction | |
| size_before = len(content) | |
| size_after = len(cleaned_content) | |
| print(f"Cleaned base64 audio data. Size before: {size_before}, after: {size_after}, saved: {size_before - size_after} bytes") | |
| return cleaned_content | |
| return content | |
| # Process the transcription using the MCP tool with URL | |
| def process_transcription_from_url(audio_url, user_groq_api_key): | |
| try: | |
| global mcp_tools | |
| global groq_api_key | |
| # Use environment variable or user-provided key | |
| current_groq_api_key = groq_api_key if groq_api_key else user_groq_api_key | |
| if not current_groq_api_key: | |
| return "Error: GROQ API key is required for transcription" | |
| # Check if tools are initialized | |
| if not mcp_tools: | |
| if not initialize_tools(): | |
| return "Error: Failed to initialize MCP tools" | |
| # Get the transcription tool | |
| tool = mcp_tools.get("transcript_generator_transcribe_audio_from_url") | |
| if not tool: | |
| return "Error: Transcription tool not available" | |
| # Create a new MCP client for this specific transaction | |
| with MCPClient({"url": mcp_url}) as mcp_client: | |
| transcription_tool = next((t for t in mcp_client if t.name == "transcript_generator_transcribe_audio_from_url"), None) | |
| if not transcription_tool: | |
| return "Error: Transcription tool not found in MCP client" | |
| print(f"Calling transcription tool with URL: {audio_url} and API key (length): {len(current_groq_api_key)}") | |
| # Call the tool with the audio URL and GROQ API key | |
| result = transcription_tool(audio_url=audio_url, api_key=current_groq_api_key) | |
| print(f"Transcription result received (length): {len(result) if result else 0}") | |
| return result | |
| except Exception as e: | |
| import traceback | |
| print(f"Transcription error: {e}") | |
| print(traceback.format_exc()) | |
| return f"Error: {str(e)}" | |
| # Generate speech from text using the TTS API | |
| def generate_speech_from_text(text): | |
| try: | |
| # Prepare the request data | |
| payload = {"prompt": text} | |
| headers = {"Content-Type": "application/json"} | |
| print(f"Sending TTS request for text (length): {len(text)}") | |
| # Make the POST request to the TTS API | |
| response = requests.post(tts_api_url, json=payload, headers=headers) | |
| # Check if the request was successful | |
| if response.status_code == 200: | |
| print(f"TTS response received. Content-Type: {response.headers.get('Content-Type')}") | |
| return response.content # Return the raw audio bytes | |
| else: | |
| return f"Error: API returned status code {response.status_code} - {response.text}" | |
| except Exception as e: | |
| import traceback | |
| print(f"TTS error: {e}") | |
| print(traceback.format_exc()) | |
| return f"Error: {str(e)}" | |
| # Extract URL from message | |
| def extract_url(message): | |
| import re | |
| # Simple URL extraction regex | |
| url_pattern = r'https?://[^\s]+' | |
| match = re.search(url_pattern, message) | |
| if match: | |
| return match.group(0) | |
| return None | |
| # Function to decide if transcription tool should be used | |
| def should_use_transcription_tool(prompt, available_tools): | |
| try: | |
| if not client: | |
| print("No Claude client available for tool decision") | |
| # Default to using tool if URL is detected, since we can't ask Claude | |
| return True, "Default to using tool due to URL detection" | |
| system_prompt = """You are an AI assistant deciding whether to use available tools. | |
| You have access to a transcription tool that can transcribe audio from a URL. | |
| You should ONLY respond with a pure JSON object that includes: | |
| 1. "use_tool": true/false - whether to use the transcription tool | |
| 2. "reasoning": brief explanation of your decision | |
| It should start with a '{' and end with a '}'. | |
| Respond with true ONLY if: | |
| - The user is clearly asking for audio transcription | |
| - OR the user shared an audio URL and wants to know what's in it | |
| - OR the user is asking about content from an audio file | |
| Respond with false if: | |
| - The user is just chatting or asking questions not related to audio transcription | |
| - No audio URL is mentioned (unless the user is explicitly asking how to use the transcription feature) | |
| """ | |
| # Add information about available tools | |
| tool_info = "" | |
| for tool_name, tool_data in available_tools.items(): | |
| tool_info += f"Tool: {tool_name}\n" | |
| tool_info += f"Description: {tool_data.get('description', 'No description')}\n" | |
| tool_info += f"Parameters: {json.dumps(tool_data.get('parameters', {}))}\n\n" | |
| system_prompt += f"\n\nAvailable tools:\n{tool_info}" | |
| # Ensure we're sending a valid message | |
| response = client.messages.create( | |
| model="claude-3-7-sonnet-latest", | |
| max_tokens=500, | |
| system=system_prompt, | |
| messages=[{"role": "user", "content": f"User message: {prompt}"}] | |
| ) | |
| response_content = response.content[0].text | |
| print(f"Tool decision response: {response_content}") | |
| # Try to parse the JSON response | |
| try: | |
| result = json.loads(response_content) | |
| return result.get("use_tool", False), result.get("reasoning", "No reasoning provided") | |
| except json.JSONDecodeError: | |
| print(f"Failed to parse JSON response: {response_content}") | |
| # Fallback: check if the response contains "true" | |
| return "true" in response_content.lower(), "Failed to parse JSON response" | |
| except Exception as e: | |
| import traceback | |
| print(f"Error in tool decision: {e}") | |
| print(traceback.format_exc()) | |
| # Default to not using tool if there's an error | |
| return False, f"Error: {str(e)}" | |
| # Function to decide if TTS tool should be used | |
| def should_use_tts_tool(prompt, history=None): | |
| try: | |
| if not client: | |
| print("No Claude client available for TTS tool decision") | |
| # Default to not using tool since we can't ask Claude | |
| return False, "No Claude client available for decision", "" | |
| system_prompt = """You are an AI assistant deciding whether to use a text-to-speech (TTS) tool. | |
| You have access to a TTS tool that can generate speech audio from text. | |
| You should ONLY respond with a pure JSON object that includes: | |
| 1. "use_tool": true/false - whether to use the TTS tool | |
| 2. "reasoning": brief explanation of your decision | |
| 3. "text_to_convert": If use_tool is true, include the text that should be converted to speech. | |
| Extract this from the user's message or generate appropriate text based on their request. | |
| It should start with a '{' and end with a '}'. | |
| Respond with true ONLY if: | |
| - The user is clearly asking to generate speech or audio from text | |
| - OR the user wants text read aloud | |
| - OR the user wants to create a voiceover or narration | |
| - OR the user explicitly mentions text-to-speech or TTS | |
| When deciding what text to convert: | |
| - If the user provides specific text in quotes or after phrases like "convert this to speech:", use that exact text | |
| - If the user asks to convert their previous message, use that as the text | |
| - If the user refers to content from the conversation history, extract that content | |
| - If the user asks to convert the transcript or other previous content, find that in the history | |
| - If the user doesn't specify text but clearly wants TTS, ask them to provide the text | |
| Respond with false if: | |
| - The user is just chatting or asking questions not related to speech generation | |
| - The user is asking about transcription (speech-to-text) instead | |
| - The user is asking about content generation | |
| """ | |
| # Prepare the messages for Claude | |
| messages = [] | |
| if history: | |
| # Include history in the context to allow referencing previous messages | |
| formatted_history = format_history(history) | |
| messages = formatted_history | |
| # Add the user's current prompt as the final message | |
| messages.append({"role": "user", "content": prompt}) | |
| else: | |
| # If no history, just use the current prompt | |
| messages = [{"role": "user", "content": f"User message: {prompt}"}] | |
| # Ensure we're sending a valid message | |
| response = client.messages.create( | |
| model="claude-3-7-sonnet-latest", | |
| max_tokens=500, | |
| system=system_prompt, | |
| messages=messages | |
| ) | |
| response_content = response.content[0].text | |
| print(f"TTS tool decision response: {response_content}") | |
| # Try to parse the JSON response | |
| try: | |
| result = json.loads(response_content) | |
| text_to_convert = result.get("text_to_convert", "") | |
| return result.get("use_tool", False), result.get("reasoning", "No reasoning provided"), text_to_convert | |
| except json.JSONDecodeError: | |
| print(f"Failed to parse JSON response: {response_content}") | |
| # Fallback: check if the response contains "true" | |
| return "true" in response_content.lower(), "Failed to parse JSON response", "" | |
| except Exception as e: | |
| import traceback | |
| print(f"Error in TTS tool decision: {e}") | |
| print(traceback.format_exc()) | |
| # Default to not using tool if there's an error | |
| return False, f"Error: {str(e)}", "" | |
| # Main chat function (streaming, with tool usage) | |
| def chat_with_tools(user_message, history, user_groq_api_key, user_anthropic_api_key): | |
| global client, anthropic_api_key, groq_api_key, tool_descriptions | |
| # Check if we need to update the Anthropic client with user-provided API key | |
| current_anthropic_api_key = anthropic_api_key if anthropic_api_key else user_anthropic_api_key | |
| if not current_anthropic_api_key: | |
| history.append(ChatMessage( | |
| role="assistant", | |
| content="Please provide an Anthropic API key to continue.", | |
| metadata={"title": "❌ Error", "status": "done"} | |
| )) | |
| yield history | |
| return | |
| # Update client if needed | |
| if client is None or (not anthropic_api_key and user_anthropic_api_key): | |
| client = anthropic.Anthropic(api_key=current_anthropic_api_key) | |
| # Add user message to history for display | |
| history = history or [] | |
| history.append(ChatMessage(role="user", content=user_message)) | |
| # Format messages for Claude API | |
| formatted_messages = format_history(history) | |
| # 1. "Thinking" phase | |
| yield history + [ChatMessage(role="assistant", content="Let me think...", metadata={"title": "🧠 Thinking", "status": "pending"})] | |
| # Extract URL from message for transcription | |
| audio_url = extract_url(user_message) | |
| # 2. Decision phase - First check if we should use the TTS tool | |
| # Pass the full history to the TTS decision function | |
| use_tts, tts_reasoning, text_to_convert = should_use_tts_tool(user_message, history[:-1]) # Exclude the current message | |
| # 3a. Tool usage phase - Process TTS if decided to use that tool | |
| if use_tts: | |
| try: | |
| # Show tool call status | |
| yield history + [ChatMessage( | |
| role="assistant", | |
| content=f"Generating speech from text...", | |
| metadata={"title": "🔊 Text-to-Speech", "status": "pending"} | |
| )] | |
| # If no text was provided for conversion, ask the user | |
| if not text_to_convert: | |
| history.append(ChatMessage( | |
| role="assistant", | |
| content="I'd be happy to generate speech audio for you! Could you please provide the text you want me to convert to speech?", | |
| metadata={"title": "ℹ️ Info", "status": "done"} | |
| )) | |
| yield history | |
| return | |
| # Process text-to-speech | |
| audio_data = generate_speech_from_text(text_to_convert) | |
| if isinstance(audio_data, str) and audio_data.startswith("Error:"): | |
| history.append(ChatMessage( | |
| role="assistant", | |
| content=audio_data, | |
| metadata={"title": "❌ Error", "status": "done"} | |
| )) | |
| yield history | |
| return | |
| elif audio_data: | |
| # Convert audio data to base64 for embedding directly in HTML | |
| base64_audio = base64.b64encode(audio_data).decode('utf-8') | |
| # Create a response with the audio player using data URL | |
| response_with_audio = f"""I've converted your text to speech. Here's the audio: | |
| <audio controls src="data:audio/wav;base64,{base64_audio}" style="width: 100%;"></audio> | |
| The text that was converted: "{text_to_convert}" | |
| How does the audio sound? Let me know if you'd like to generate speech from any other text. | |
| """ | |
| # Store a custom attribute on the message to indicate it has audio | |
| # This will be displayed in the UI but the base64 data will be stripped | |
| # when sending to Claude | |
| history.append(ChatMessage( | |
| role="assistant", | |
| content=response_with_audio | |
| )) | |
| yield history | |
| return | |
| else: | |
| history.append(ChatMessage( | |
| role="assistant", | |
| content="I wasn't able to generate speech from your text. Please try again with different text.", | |
| metadata={"title": "❌ Error", "status": "done"} | |
| )) | |
| yield history | |
| return | |
| except Exception as e: | |
| import traceback | |
| error_details = traceback.format_exc() | |
| print(f"TTS error details: {error_details}") | |
| history.append(ChatMessage( | |
| role="assistant", | |
| content=f"Error generating speech: {str(e)}", | |
| metadata={"title": "❌ Error", "status": "done"} | |
| )) | |
| yield history | |
| return | |
| # 2b. If not using TTS, check if we should use the transcription tool | |
| if not use_tts and audio_url: | |
| use_transcription, transcription_reasoning = should_use_transcription_tool(user_message, tool_descriptions) | |
| print(f"Tool decision: {use_transcription}, Reasoning: {transcription_reasoning}") | |
| else: | |
| use_transcription = False | |
| # 3b. Process transcription if decided to use that tool | |
| if use_transcription and audio_url: | |
| try: | |
| # Check if GROQ API key is available (either from env or user input) | |
| current_groq_api_key = groq_api_key if groq_api_key else user_groq_api_key | |
| if not current_groq_api_key: | |
| history.append(ChatMessage( | |
| role="assistant", | |
| content="Please provide a GROQ API key to process the transcription.", | |
| metadata={"title": "ℹ️ Info", "status": "done"} | |
| )) | |
| yield history | |
| return | |
| # Show tool call status | |
| yield history + [ChatMessage( | |
| role="assistant", | |
| content=f"Processing audio from URL: {audio_url}...", | |
| metadata={"title": "🛠️ Tool Usage", "status": "pending"} | |
| )] | |
| # Process transcription with GROQ API key | |
| transcription_result = process_transcription_from_url(audio_url, user_groq_api_key) | |
| if transcription_result and transcription_result.startswith("Error:"): | |
| history.append(ChatMessage( | |
| role="assistant", | |
| content=transcription_result, | |
| metadata={"title": "❌ Error", "status": "done"} | |
| )) | |
| yield history | |
| return | |
| elif transcription_result: | |
| # Add transcript result to history with direct response | |
| history.append(ChatMessage( | |
| role="assistant", | |
| content=f"I've transcribed the audio for you. Here's what I found:\n\n{transcription_result}\n\nIs there anything specific you'd like to know about this transcript?" | |
| )) | |
| yield history | |
| return | |
| else: | |
| history.append(ChatMessage( | |
| role="assistant", | |
| content="I wasn't able to get a transcription result. Please check the URL and try again.", | |
| metadata={"title": "❌ Error", "status": "done"} | |
| )) | |
| yield history | |
| return | |
| except Exception as e: | |
| import traceback | |
| error_details = traceback.format_exc() | |
| print(f"Error details: {error_details}") | |
| history.append(ChatMessage( | |
| role="assistant", | |
| content=f"Error processing the audio URL: {str(e)}", | |
| metadata={"title": "❌ Error", "status": "done"} | |
| )) | |
| yield history | |
| return | |
| elif use_transcription and not audio_url: | |
| # User wants transcription but didn't provide URL | |
| history.append(ChatMessage( | |
| role="assistant", | |
| content="I'd like to help with audio transcription, but I need a URL to the audio file. Please provide a direct link to the audio you want to transcribe.", | |
| metadata={"title": "ℹ️ Info", "status": "done"} | |
| )) | |
| yield history | |
| return | |
| # 4. Claude response phase - for regular chat or when tool isn't needed | |
| try: | |
| # Provide system prompt with tools information | |
| system_prompt = f"""You are a helpful assistant that can transcribe audio and generate speech from text using specialized tools. | |
| Available tools: | |
| - Audio transcription: You can convert speech in audio files to text when users provide audio URLs | |
| - Text-to-Speech: You can convert text to speech audio when users want to generate spoken content | |
| Tool usage guidelines: | |
| - The system has already determined that neither the transcription nor TTS tools should be used for this message | |
| - If the user asks about transcribing audio but didn't provide a URL, tell them you need an audio URL | |
| - If the user asks about generating speech but didn't provide text, tell them you need the text to convert | |
| - For other queries, respond normally | |
| When responding to general questions: | |
| - Provide helpful and accurate information | |
| - Be concise and direct | |
| - If the user's message suggests they might want to use either tool later, you can mention these capabilities | |
| """ | |
| # Debug message format | |
| print(f"Sending {len(formatted_messages)} messages to Claude") | |
| response = client.messages.create( | |
| model="claude-3-7-sonnet-latest", | |
| max_tokens=4096, | |
| system=system_prompt, | |
| messages=formatted_messages | |
| ) | |
| response_content = response.content[0].text | |
| history.append(ChatMessage(role="assistant", content=response_content)) | |
| except Exception as e: | |
| import traceback | |
| error_details = traceback.format_exc() | |
| print(f"Claude error details: {error_details}") | |
| history.append(ChatMessage( | |
| role="assistant", | |
| content=f"Error connecting to Claude: {str(e)}", | |
| metadata={"title": "❌ Error", "status": "done"} | |
| )) | |
| yield history | |
| # Initialize tools at startup | |
| initialize_tools() | |
| # Create the Gradio interface | |
| with gr.Blocks() as demo: | |
| gr.Markdown("# Claude 3.7 Sonnet Chat with Tools") | |
| gr.Markdown("Send a message with an audio URL to generate a transcript, or ask to generate speech from text.") | |
| with gr.Row(): | |
| with gr.Column(scale=4): | |
| chatbot = gr.Chatbot( | |
| type="messages", | |
| label="Claude Agent", | |
| height=600, | |
| render=True # Enable HTML rendering for audio player | |
| ) | |
| with gr.Column(scale=1): | |
| # Display API key inputs only if environment variables are not set | |
| anthropic_api_key_input = gr.Textbox( | |
| label="Anthropic API Key" + (" (using env variable)" if anthropic_api_key else ""), | |
| placeholder="Enter your Anthropic API key" if not anthropic_api_key else "Using environment variable", | |
| type="password", | |
| show_label=True, | |
| interactive=not anthropic_api_key, | |
| value="" if not anthropic_api_key else None | |
| ) | |
| groq_api_key_input = gr.Textbox( | |
| label="GROQ API Key" + (" (using env variable)" if groq_api_key else ""), | |
| placeholder="Enter your GROQ API key" if not groq_api_key else "Using environment variable", | |
| type="password", | |
| show_label=True, | |
| interactive=not groq_api_key, | |
| value="" if not groq_api_key else None | |
| ) | |
| with gr.Row(): | |
| msg = gr.Textbox( | |
| placeholder="Ask a question, share an audio URL for transcription, or request text-to-speech...", | |
| show_label=False, | |
| container=False, | |
| scale=9 | |
| ) | |
| btn = gr.Button("Send", scale=1) | |
| # Connect components | |
| btn.click(chat_with_tools, [msg, chatbot, groq_api_key_input, anthropic_api_key_input], [chatbot], queue=True) | |
| msg.submit(chat_with_tools, [msg, chatbot, groq_api_key_input, anthropic_api_key_input], [chatbot], queue=True) | |
| # Clear message input field after sending | |
| def clear_message_input(): | |
| return "" | |
| msg.submit(clear_message_input, [], [msg], queue=False) | |
| # Clear button | |
| clear = gr.Button("Clear Conversation") | |
| # Reset conversation and clear message input but keep API keys | |
| def clear_conversation(): | |
| return [], "" | |
| clear.click( | |
| clear_conversation, | |
| [], | |
| [chatbot, msg] # Clear chatbot history and message input | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch() |