Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
178 changes: 178 additions & 0 deletions community-chatbot/app/api/chat/__tests__/route.test.ts
Original file line number Diff line number Diff line change
@@ -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<string, string>;
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');
});
});
});
71 changes: 55 additions & 16 deletions tools/translation-helper/app.py
Original file line number Diff line number Diff line change
@@ -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 = [
Expand All @@ -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__":
Expand Down
4 changes: 4 additions & 0 deletions tools/translation-helper/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
gradio
groq
python-dotenv
gTTS
Loading