diff --git a/community-chatbot/app/api/chat/__tests__/route.test.ts b/community-chatbot/app/api/chat/__tests__/route.test.ts new file mode 100644 index 00000000..105c4cb7 --- /dev/null +++ b/community-chatbot/app/api/chat/__tests__/route.test.ts @@ -0,0 +1,178 @@ +/** + * @jest-environment node + */ + +import { POST } from '@/app/api/chat/route'; +import { handleGeneralRequest } from '@/app/api/chat/handlers/general'; +import { handleGitHubRequest } from '@/app/api/chat/handlers/github'; +import { handleJiraRequest } from '@/app/api/chat/handlers/jira'; +import { handleSlackRequest } from '@/app/api/chat/handlers/slack'; + +// Mock the handlers +jest.mock('@/app/api/chat/handlers/general'); +jest.mock('@/app/api/chat/handlers/github'); +jest.mock('@/app/api/chat/handlers/jira'); +jest.mock('@/app/api/chat/handlers/slack'); + +// Mock @ai-sdk/openai +jest.mock('@ai-sdk/openai', () => ({ + openai: jest.fn().mockReturnValue('mock-openai'), +})); + +// Mock constants +jest.mock('@/app/api/chat/lib/constants', () => ({ + SYSTEM_PROMPTS: { + general: 'Mock general prompt', + }, + maxDuration: 30, +})); + +describe('Chat API route', () => { + const originalResponse = global.Response; + + beforeEach(() => { + jest.clearAllMocks(); + + // Custom MockResponse to handle body reading in tests + class MockResponse { + status: number; + headers: Map; + body: any; + + constructor(body: any, init?: any) { + this.body = body; + this.status = init?.status || 200; + this.headers = new Map(Object.entries(init?.headers || {})); + } + + async json() { + return JSON.parse(this.body); + } + + async text() { + return String(this.body); + } + } + global.Response = MockResponse as any; + }); + + afterAll(() => { + global.Response = originalResponse; + }); + + const createRequest = (body: any) => { + return new Request('https://localhost:3000/api/chat', { + method: 'POST', + body: JSON.stringify(body), + headers: { + 'Content-Type': 'application/json', + }, + }); + }; + + describe('Validation', () => { + it('should return 400 if messages is missing', async () => { + const req = createRequest({ mode: 'general' }); + const res = await POST(req); + expect(res.status).toBe(400); + const data = await res.json(); + expect(data.error).toBe("Request must contain a 'messages' array."); + }); + + it('should return 400 if mode is missing', async () => { + const req = createRequest({ messages: [{ id: '1', role: 'user', content: 'hi', timestamp: Date.now() }] }); + const res = await POST(req); + expect(res.status).toBe(400); + const data = await res.json(); + expect(data.error).toBe("Request must contain a 'mode' string."); + }); + + it('should return 400 if messages is empty', async () => { + const req = createRequest({ messages: [], mode: 'general' }); + const res = await POST(req); + expect(res.status).toBe(400); + const data = await res.json(); + expect(data.error).toBe("Request must contain a non-empty messages array."); + }); + + it('should return 400 if last message is not from user', async () => { + const req = createRequest({ + messages: [{ id: '1', role: 'assistant', content: 'hi', timestamp: Date.now() }], + mode: 'general' + }); + const res = await POST(req); + expect(res.status).toBe(400); + const data = await res.json(); + expect(data.error).toBe("Invalid message sequence. The last message must be from a user."); + }); + }); + + describe('Modes', () => { + const messages = [{ id: '1', role: 'user' as const, content: 'test query', timestamp: Date.now() }]; + + it('should call handleSlackRequest for slack mode', async () => { + const req = createRequest({ messages, mode: 'slack' }); + await POST(req); + expect(handleSlackRequest).toHaveBeenCalledWith('test query'); + }); + + it('should call handleJiraRequest for jira mode', async () => { + const req = createRequest({ messages, mode: 'jira' }); + await POST(req); + expect(handleJiraRequest).toHaveBeenCalledWith('test query'); + }); + + it('should call handleGitHubRequest for github mode', async () => { + const req = createRequest({ messages, mode: 'github' }); + await POST(req); + expect(handleGitHubRequest).toHaveBeenCalledWith('test query'); + }); + + it('should call handleGeneralRequest for unknown mode', async () => { + const req = createRequest({ messages, mode: 'unknown' }); + await POST(req); + expect(handleGeneralRequest).toHaveBeenCalledWith(messages); + }); + }); + + describe('Error Handling', () => { + const messages = [{ id: '1', role: 'user' as const, content: 'test query', timestamp: Date.now() }]; + + it('should fall back to general request if integration handler fails', async () => { + (handleJiraRequest as jest.Mock).mockRejectedValue(new Error('Jira Down')); + const req = createRequest({ messages, mode: 'jira' }); + + await POST(req); + + expect(handleGeneralRequest).toHaveBeenCalled(); + const calledWithMessages = (handleGeneralRequest as jest.Mock).mock.calls[0][0]; + expect(calledWithMessages[0].content).toContain('I was trying to use the jira integration but it seems to be unavailable'); + }); + + it('should return 500 if a critical error occurs', async () => { + // Force an error during JSON parsing or something else critical + const req = new Request('https://localhost:3000/api/chat', { + method: 'POST', + body: 'invalid-json', + headers: { + 'Content-Type': 'application/json', + }, + }); + + const res = await POST(req); + expect(res.status).toBe(500); + const data = await res.json(); + expect(data.error).toContain('An unexpected server error occurred'); + }); + + it('should return 500 if the general handler fails (e.g. missing API keys)', async () => { + (handleGeneralRequest as jest.Mock).mockRejectedValue(new Error('Missing OpenAI API Key')); + const req = createRequest({ messages, mode: 'general' }); + + const res = await POST(req); + expect(res.status).toBe(500); + const data = await res.json(); + expect(data.error).toContain('An unexpected server error occurred'); + }); + }); +}); diff --git a/tools/translation-helper/app.py b/tools/translation-helper/app.py index df896dcf..38900dd4 100644 --- a/tools/translation-helper/app.py +++ b/tools/translation-helper/app.py @@ -1,14 +1,16 @@ import os +import tempfile import gradio as gr from dotenv import load_dotenv -import google.generativeai as genai +from groq import Groq +from gtts import gTTS # Load API Key from .env load_dotenv() -genai.configure(api_key=os.getenv("GEMINI_API_KEY")) -# Load Gemini model -model = genai.GenerativeModel("models/gemini-2.0-flash") +# Initialize Groq client +client = Groq(api_key=os.getenv("GROQ_API_KEY")) +model_name = "llama-3.3-70b-versatile" # Latest and recommended versatile model from Groq # Supported languages (from the list you shared) supported_languages = [ @@ -21,38 +23,75 @@ "Vietnamese" ] +# Language codes for gTTS +LANGUAGE_CODES = { + "Arabic": "ar", "Bengali": "bn", "Bulgarian": "bg", "Chinese (Simplified)": "zh-CN", + "Chinese (Traditional)": "zh-TW", "Croatian": "hr", "Czech": "cs", "Danish": "da", + "Dutch": "nl", "English": "en", "Estonian": "et", "Finnish": "fi", "French": "fr", + "German": "de", "Greek": "el", "Hebrew": "iw", "Hindi": "hi", "Hungarian": "hu", + "Indonesian": "id", "Italian": "it", "Japanese": "ja", "Korean": "ko", "Latvian": "lv", + "Lithuanian": "lt", "Norwegian": "no", "Polish": "pl", "Portuguese": "pt", + "Romanian": "ro", "Russian": "ru", "Serbian": "sr", "Slovak": "sk", "Slovenian": "sl", + "Spanish": "es", "Swahili": "sw", "Swedish": "sv", "Thai": "th", "Turkish": "tr", + "Ukrainian": "uk", "Vietnamese": "vi" +} + # Translation logic def translate_text(input_text, target_language, formal): if target_language not in supported_languages: - return "❌ Error: The selected language is not supported." + return "❌ Error: The selected language is not supported.", None tone = "formal" if formal else "informal" prompt = f"Translate the following text into {target_language} in a {tone} tone. Only provide the translated text without any explanation or additional commentary:\n\n{input_text}" try: - response = model.generate_content(prompt) - return response.text.strip() + completion = client.chat.completions.create( + model=model_name, + messages=[{ + "role": "user", + "content": prompt + }], + temperature=0.3, + stream=False, + ) + translated_text = completion.choices[0].message.content.strip() + + # Generate Audio using gTTS + lang_code = LANGUAGE_CODES.get(target_language, "en") + try: + tts = gTTS(text=translated_text, lang=lang_code) + # Create a temporary file to store audio + temp_audio = tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") + tts.save(temp_audio.name) + audio_path = temp_audio.name + except Exception as audio_err: + print(f"Audio generation failed: {audio_err}") + audio_path = None + + return translated_text, audio_path + except Exception as e: - return f"❌ Error: {e}" + return f"❌ Error: {e}", None # Gradio Interface -with gr.Blocks(title="Multilingual Translator") as app: - gr.Markdown("# 🌐 AI Translator with Gemini 2.0 Flash") - gr.Markdown("Type in English and translate to supported languages using Gemini 2.0 Flash") +with gr.Blocks(title="Translation Helper") as app: + gr.Markdown("# Translation & Text-to-Speech Tool") + gr.Markdown("Translate English text to supported languages and generate audio pronunciation.") with gr.Row(): - input_text = gr.Textbox(label="Enter English text", placeholder="e.g. How are you doing?", interactive=True) + input_text = gr.Textbox(label="English Input", placeholder="Enter text here...", interactive=True) language = gr.Dropdown(choices=supported_languages, label="Target Language") tone = gr.Checkbox(label="Use Formal Tone", value=True) - translate_btn = gr.Button("Translate 🔁") - output_text = gr.Textbox(label="Translated Output") + translate_btn = gr.Button("Translate") + output_text = gr.Textbox(label="Translated Text") + output_audio = gr.Audio(label="Audio Output", type="filepath") # Trigger translation when "Enter" is pressed on the input_text textbox - input_text.submit(fn=translate_text, inputs=[input_text, language, tone], outputs=output_text) + input_text.submit(fn=translate_text, inputs=[input_text, language, tone], outputs=[output_text, output_audio]) # Button click still works as fallback - translate_btn.click(fn=translate_text, inputs=[input_text, language, tone], outputs=output_text) + translate_btn.click(fn=translate_text, inputs=[input_text, language, tone], outputs=[output_text, output_audio]) # Run app if __name__ == "__main__": diff --git a/tools/translation-helper/requirements.txt b/tools/translation-helper/requirements.txt new file mode 100644 index 00000000..808adb05 --- /dev/null +++ b/tools/translation-helper/requirements.txt @@ -0,0 +1,4 @@ +gradio +groq +python-dotenv +gTTS