From bffb2d676e795072c7035ec62e4c03d68fdf6113 Mon Sep 17 00:00:00 2001 From: konard Date: Sat, 13 Sep 2025 01:04:47 +0300 Subject: [PATCH 1/3] Initial commit with task details for issue #130 Adding CLAUDE.md with task information for AI processing. This file will be removed when the task is complete. Issue: https://github.com/linksplatform/Bot/issues/130 --- CLAUDE.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..352781cf --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,5 @@ +Issue to solve: https://github.com/linksplatform/Bot/issues/130 +Your prepared branch: issue-130-dff655c7 +Your prepared working directory: /tmp/gh-issue-solver-1757714683497 + +Proceed. \ No newline at end of file From 966f548a6a7b76def6ae7eda22e10b22ef9db162 Mon Sep 17 00:00:00 2001 From: konard Date: Sat, 13 Sep 2025 01:16:48 +0300 Subject: [PATCH 2/3] Implement VK Bot translation word API feature MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add translation patterns for Russian and English queries - Implement translate_word() method with language detection - Support queries like "Как перевести X на английский?" and "How to translate X?" - Use MyMemory free translation API with proper URL encoding - Respond in the same language as the query - Add comprehensive tests for pattern matching and API functionality 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- examples/manual_test.py | 99 +++++++++++++++++++++++++++++++ examples/test_api.py | 44 ++++++++++++++ examples/test_final_logic.py | 112 +++++++++++++++++++++++++++++++++++ examples/test_translation.py | 73 +++++++++++++++++++++++ python/__main__.py | 6 +- python/modules/commands.py | 77 +++++++++++++++++++++++- python/patterns.py | 13 ++++ 7 files changed, 422 insertions(+), 2 deletions(-) create mode 100644 examples/manual_test.py create mode 100644 examples/test_api.py create mode 100644 examples/test_final_logic.py create mode 100644 examples/test_translation.py diff --git a/examples/manual_test.py b/examples/manual_test.py new file mode 100644 index 00000000..77ced27d --- /dev/null +++ b/examples/manual_test.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Manual test for translation logic without external dependencies.""" + +import re + +def test_russian_detection(): + """Test Russian character detection logic.""" + test_cases = [ + ("Как перевести hello на английский?", True), + ("How to translate красота?", False), + ("как переводится beautiful", True), + ("translate house", False), + ("перевод машина", True), + ] + + print("Testing Russian character detection:") + print("-" * 40) + + for text, expected in test_cases: + is_russian = bool(re.search(r'[а-яё]', text, re.IGNORECASE)) + result = "✓" if is_russian == expected else "✗" + print(f"{result} '{text}' -> Russian: {is_russian} (expected: {expected})") + + print() + +def test_word_extraction(): + """Test word extraction from various patterns.""" + patterns_and_tests = [ + # Pattern similar to TRANSLATE_TO_ENGLISH_RU + (r'(как перевести|как переводится)\s+(.+?)\s+(на английский|на англ)', + "Как перевести hello на английский?", "hello"), + + # Pattern similar to TRANSLATE_TO_RUSSIAN_RU + (r'(как переводится|как перевести)\s+(.+?)(\?|$)', + "Как переводится beautiful?", "beautiful"), + + # Pattern similar to TRANSLATE_TO_ENGLISH_EN + (r'(how to translate|what is translation of)\s+(.+?)(\?|$)', + "How to translate красота?", "красота"), + + # Pattern similar to TRANSLATE_WORD + (r'(translate|перевести|переводить|перевод)\s+(.+?)(\?|$)', + "translate house", "house"), + ] + + print("Testing word extraction patterns:") + print("-" * 40) + + for pattern, text, expected_word in patterns_and_tests: + match = re.search(pattern, text, re.IGNORECASE) + if match: + extracted = match.group(2).strip() + result = "✓" if extracted == expected_word else "✗" + print(f"{result} '{text}' -> '{extracted}' (expected: '{expected_word}')") + else: + print(f"✗ '{text}' -> No match") + + print() + +def test_language_logic(): + """Test the language detection and translation direction logic.""" + print("Testing translation direction logic (updated):") + print("-" * 40) + + test_cases = [ + ("Как перевести hello на английский?", "hello", "en->ru"), + ("How to translate красота?", "красота", "ru->en"), + ("Как переводится beautiful?", "beautiful", "en->ru"), + ("translate house", "house", "en->ru"), + ("перевод машина", "машина", "ru->en"), + ] + + for msg, word, expected_direction in test_cases: + # Use updated logic: check message without the word + message_without_word = msg.replace(word, '') + is_russian_message = bool(re.search(r'[а-яё]', message_without_word, re.IGNORECASE)) + word_is_russian = bool(re.search(r'[а-яё]', word, re.IGNORECASE)) + + # Simplified logic: translate based on word language + if word_is_russian: + source_lang = 'ru' + target_lang = 'en' + else: + source_lang = 'en' + target_lang = 'ru' + + actual_direction = f"{source_lang}->{target_lang}" + + result = "✓" if expected_direction == actual_direction else "✗" + print(f"{result} '{msg}' with word '{word}':") + print(f" Expected: {expected_direction}") + print(f" Actual: {actual_direction}") + print() + +if __name__ == '__main__': + test_russian_detection() + test_word_extraction() + test_language_logic() \ No newline at end of file diff --git a/examples/test_api.py b/examples/test_api.py new file mode 100644 index 00000000..95bc91cf --- /dev/null +++ b/examples/test_api.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Test the actual translation API.""" + +import json +import urllib.request +import urllib.parse + +def test_translation_api(): + """Test MyMemory translation API.""" + print("Testing MyMemory Translation API:") + print("=" * 40) + + test_cases = [ + ('hello', 'en', 'ru'), + ('beautiful', 'en', 'ru'), + ('красота', 'ru', 'en'), + ('дом', 'ru', 'en'), + ('house', 'en', 'ru'), + ] + + for word, source, target in test_cases: + try: + # URL encode the word to handle special characters + encoded_word = urllib.parse.quote(word) + url = f"https://api.mymemory.translated.net/get?q={encoded_word}&langpair={source}|{target}" + + print(f"\nTranslating '{word}' from {source} to {target}...") + print(f"URL: {url}") + + with urllib.request.urlopen(url, timeout=10) as response: + data = json.loads(response.read().decode()) + + if data.get('responseStatus') == 200: + translation = data.get('responseData', {}).get('translatedText', '') + print(f"✓ Result: '{word}' -> '{translation}'") + else: + print(f"✗ API Error: {data.get('responseStatus')} - {data.get('responseDetails', 'Unknown error')}") + + except Exception as e: + print(f"✗ Network Error: {e}") + +if __name__ == '__main__': + test_translation_api() \ No newline at end of file diff --git a/examples/test_final_logic.py b/examples/test_final_logic.py new file mode 100644 index 00000000..828d0a68 --- /dev/null +++ b/examples/test_final_logic.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Test the final translation logic according to issue requirements.""" + +import re + +def test_translation_logic(): + """Test complete translation logic according to issue requirements.""" + print("Testing Final Translation Logic:") + print("=" * 50) + + test_cases = [ + # Issue examples + { + 'msg': 'Как перевести hello на английский?', + 'word': 'hello', + 'expected_source': 'en', + 'expected_target': 'en', # Explicitly asking for English + 'expected_response_lang': 'russian', + 'expected_format': '"hello" переводится как "привет"' + }, + { + 'msg': 'Как переводится beautiful?', + 'word': 'beautiful', + 'expected_source': 'en', + 'expected_target': 'ru', # Inferred Russian target + 'expected_response_lang': 'russian', + 'expected_format': '"beautiful" переводится как "красивый"' + }, + { + 'msg': 'How to translate красота?', + 'word': 'красота', + 'expected_source': 'ru', + 'expected_target': 'en', # Inferred English target + 'expected_response_lang': 'english', + 'expected_format': '"красота" translates to "beauty"' + }, + { + 'msg': 'What is translation of дом?', + 'word': 'дом', + 'expected_source': 'ru', + 'expected_target': 'en', # Inferred English target + 'expected_response_lang': 'english', + 'expected_format': '"дом" translates to "house"' + }, + # Additional test cases + { + 'msg': 'translate house', + 'word': 'house', + 'expected_source': 'en', + 'expected_target': 'ru', # English word -> Russian + 'expected_response_lang': 'english', + 'expected_format': '"house" translates to "дом"' + } + ] + + for i, case in enumerate(test_cases, 1): + print(f"\nTest {i}: {case['msg']}") + print("-" * 40) + + msg = case['msg'] + word = case['word'] + + # Apply the logic from the implementation + message_without_word = msg.replace(word, '') + is_russian_message = bool(re.search(r'[а-яё]', message_without_word, re.IGNORECASE)) + word_is_russian = bool(re.search(r'[а-яё]', word, re.IGNORECASE)) + + if re.search(r'на английский|на англ', msg, re.IGNORECASE): + # Explicitly asking for English translation + source_lang = 'ru' if word_is_russian else 'en' + target_lang = 'en' + response_in_russian = True + elif is_russian_message: + # Russian message without explicit target - infer target language + if word_is_russian: + source_lang = 'ru' + target_lang = 'en' + else: + source_lang = 'en' + target_lang = 'ru' + response_in_russian = True + else: + # English message - infer target as English + if word_is_russian: + source_lang = 'ru' + target_lang = 'en' + else: + # English word in English message - might want Russian translation + source_lang = 'en' + target_lang = 'ru' + response_in_russian = False + + # Check results + source_ok = source_lang == case['expected_source'] + target_ok = target_lang == case['expected_target'] + response_lang_ok = ( + (response_in_russian and case['expected_response_lang'] == 'russian') or + (not response_in_russian and case['expected_response_lang'] == 'english') + ) + + print(f"Word: '{word}' ({'Russian' if word_is_russian else 'English'} detected)") + print(f"Message language: {'Russian' if is_russian_message else 'English'}") + print(f"Source language: {source_lang} {'✓' if source_ok else '✗ (expected ' + case['expected_source'] + ')'}") + print(f"Target language: {target_lang} {'✓' if target_ok else '✗ (expected ' + case['expected_target'] + ')'}") + print(f"Response language: {'Russian' if response_in_russian else 'English'} {'✓' if response_lang_ok else '✗ (expected ' + case['expected_response_lang'] + ')'}") + + all_ok = source_ok and target_ok and response_lang_ok + print(f"Overall result: {'✓ PASS' if all_ok else '✗ FAIL'}") + +if __name__ == '__main__': + test_translation_logic() \ No newline at end of file diff --git a/examples/test_translation.py b/examples/test_translation.py new file mode 100644 index 00000000..e832b436 --- /dev/null +++ b/examples/test_translation.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Test script for translation patterns.""" + +import sys +import os +sys.path.append(os.path.join(os.path.dirname(__file__), '..', 'python')) + +from regex import match +import patterns + +def test_pattern(pattern, text, expected_word): + """Test if pattern matches text and extracts expected word.""" + matched = match(pattern, text) + if matched: + word = matched.group('word') + print(f"✓ Pattern matched: '{text}' -> word: '{word}'") + if word.strip() == expected_word: + print(f" ✓ Extracted word matches expected: '{expected_word}'") + return True + else: + print(f" ✗ Expected '{expected_word}', got '{word.strip()}'") + return False + else: + print(f"✗ Pattern did NOT match: '{text}'") + return False + +def run_tests(): + """Run all pattern tests.""" + print("Testing Translation Patterns") + print("=" * 50) + + test_cases = [ + # Russian patterns asking for English translation + (patterns.TRANSLATE_TO_ENGLISH_RU, "Как перевести hello на английский?", "hello"), + (patterns.TRANSLATE_TO_ENGLISH_RU, "как переводится world на англ", "world"), + (patterns.TRANSLATE_TO_ENGLISH_RU, "Как перевести красивый на английский", "красивый"), + + # Russian patterns asking for translation (inferred as Russian target) + (patterns.TRANSLATE_TO_RUSSIAN_RU, "Как переводится beautiful?", "beautiful"), + (patterns.TRANSLATE_TO_RUSSIAN_RU, "как перевести computer", "computer"), + + # English patterns asking for translation + (patterns.TRANSLATE_TO_ENGLISH_EN, "How to translate красота?", "красота"), + (patterns.TRANSLATE_TO_ENGLISH_EN, "What is translation of дом", "дом"), + + # Generic translate pattern + (patterns.TRANSLATE_WORD, "translate house", "house"), + (patterns.TRANSLATE_WORD, "перевести дом", "дом"), + (patterns.TRANSLATE_WORD, "перевод машина", "машина"), + ] + + passed = 0 + total = len(test_cases) + + for i, (pattern, text, expected_word) in enumerate(test_cases, 1): + print(f"\nTest {i}/{total}:") + if test_pattern(pattern, text, expected_word): + passed += 1 + + print(f"\n" + "=" * 50) + print(f"Results: {passed}/{total} tests passed") + + if passed == total: + print("🎉 All tests passed!") + return True + else: + print(f"❌ {total - passed} tests failed") + return False + +if __name__ == '__main__': + success = run_tests() + sys.exit(0 if success else 1) \ No newline at end of file diff --git a/python/__main__.py b/python/__main__.py index cdcbf7f6..5fdaef02 100644 --- a/python/__main__.py +++ b/python/__main__.py @@ -63,7 +63,11 @@ def __init__( (patterns.WHAT_IS, self.commands.what_is), (patterns.WHAT_MEAN, self.commands.what_is), (patterns.APPLY_KARMA, self.commands.apply_karma), - (patterns.GITHUB_COPILOT, self.commands.github_copilot) + (patterns.GITHUB_COPILOT, self.commands.github_copilot), + (patterns.TRANSLATE_TO_ENGLISH_RU, self.commands.translate_word), + (patterns.TRANSLATE_TO_RUSSIAN_RU, self.commands.translate_word), + (patterns.TRANSLATE_TO_ENGLISH_EN, self.commands.translate_word), + (patterns.TRANSLATE_WORD, self.commands.translate_word) ) def message_new( diff --git a/python/modules/commands.py b/python/modules/commands.py index 93d99817..9667f1fe 100644 --- a/python/modules/commands.py +++ b/python/modules/commands.py @@ -5,10 +5,12 @@ import os from regex import Pattern, Match, split, match, search, IGNORECASE, sub -from requests import post +from requests import post, get from social_ethosa import BetterUser from saya import Vk import wikipedia +import json +import urllib.parse from .commands_builder import CommandsBuilder from .data_service import BetterBotBaseDataService @@ -379,6 +381,79 @@ def github_copilot(self) -> NoReturn: f'Пожалуйста, подождите {round(config.GITHUB_COPILOT_TIMEOUT - (now - self.now))} секунд', self.peer_id ) + def translate_word(self) -> NoReturn: + """Translates word/phrase using online translation service""" + word = self.matched.group('word').strip() + + # Determine source and target languages based on message pattern and content + message_without_word = self.msg.replace(word, '') + is_russian_message = bool(search(r'[а-яё]', message_without_word, IGNORECASE)) + word_is_russian = bool(search(r'[а-яё]', word, IGNORECASE)) + + # Determine translation direction based on message patterns: + # - "Как перевести X на английский?" -> translate to English, respond in Russian + # - "Как переводится X?" -> translate to Russian (inferred), respond in Russian + # - "How to translate X?" -> translate to English (inferred), respond in English + # - "What is translation of X?" -> translate to English (inferred), respond in English + + if search(r'на английский|на англ', self.msg, IGNORECASE): + # Explicitly asking for English translation + source_lang = 'ru' if word_is_russian else 'en' + target_lang = 'en' + response_in_russian = True + elif is_russian_message: + # Russian message without explicit target - infer target language + if word_is_russian: + source_lang = 'ru' + target_lang = 'en' + else: + source_lang = 'en' + target_lang = 'ru' + response_in_russian = True + else: + # English message - infer target as English + if word_is_russian: + source_lang = 'ru' + target_lang = 'en' + else: + # English word in English message - might want Russian translation + source_lang = 'en' + target_lang = 'ru' + response_in_russian = False + + try: + # Use MyMemory translation API (free, no API key required) + encoded_word = urllib.parse.quote(word) + url = f"https://api.mymemory.translated.net/get?q={encoded_word}&langpair={source_lang}|{target_lang}" + response = get(url, timeout=10) + + if response.status_code == 200: + data = response.json() + if data.get('responseStatus') == 200: + translation = data.get('responseData', {}).get('translatedText', '') + if translation and translation.lower() != word.lower(): + # Format response based on response language + if response_in_russian: + message = f'"{word}" переводится как "{translation}"' + else: + message = f'"{word}" translates to "{translation}"' + + self.vk_instance.send_msg(message, self.peer_id) + return + + # Fallback message if translation fails + if response_in_russian: + self.vk_instance.send_msg('Не удалось перевести слово.', self.peer_id) + else: + self.vk_instance.send_msg('Could not translate the word.', self.peer_id) + + except Exception as e: + print(f"Translation error: {e}") + if response_in_russian: + self.vk_instance.send_msg('Ошибка при переводе.', self.peer_id) + else: + self.vk_instance.send_msg('Translation error occurred.', self.peer_id) + def match_command( self, pattern: Pattern diff --git a/python/patterns.py b/python/patterns.py index 1834c72c..c3d24d16 100644 --- a/python/patterns.py +++ b/python/patterns.py @@ -65,3 +65,16 @@ GITHUB_COPILOT = recompile( r'\A\s*(code|код)\s+(?P(' + COPILOT_LANGUAGES + r'))(?P[\S\s]+)\Z', IGNORECASE) + +# Translation patterns +TRANSLATE_TO_ENGLISH_RU = recompile( + r'\A\s*(как перевести|как переводится)\s+(?P[\w\s]+?)\s+(на английский|на англ)\??\s*\Z', IGNORECASE) + +TRANSLATE_TO_RUSSIAN_RU = recompile( + r'\A\s*(как переводится|как перевести)\s+(?P[\w\s]+?)\??\s*\Z', IGNORECASE) + +TRANSLATE_TO_ENGLISH_EN = recompile( + r'\A\s*(how to translate|what is translation of)\s+(?P[\w\s]+?)\??\s*\Z', IGNORECASE) + +TRANSLATE_WORD = recompile( + r'\A\s*(translate|перевести|переводить|перевод)\s+(?P[\w\s]+?)\??\s*\Z', IGNORECASE) From 99887b2904009908f85a83b418212bead877fc76 Mon Sep 17 00:00:00 2001 From: konard Date: Sat, 13 Sep 2025 01:19:11 +0300 Subject: [PATCH 3/3] Remove CLAUDE.md - Claude command completed --- CLAUDE.md | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 352781cf..00000000 --- a/CLAUDE.md +++ /dev/null @@ -1,5 +0,0 @@ -Issue to solve: https://github.com/linksplatform/Bot/issues/130 -Your prepared branch: issue-130-dff655c7 -Your prepared working directory: /tmp/gh-issue-solver-1757714683497 - -Proceed. \ No newline at end of file