Skip to content
Open
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
99 changes: 99 additions & 0 deletions experiments/test_friend_recommendations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
#!/usr/bin/env python3
"""
Test script for the friend recommendations feature.
This script creates mock data to test the recommendation algorithm logic.
"""
import sys
import os

# Add the parent directory to the path so we can import the modules
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))

def test_shared_languages_calculation():
"""Test the shared languages calculation logic."""
print("Testing shared languages calculation...")

# Test case 1: Basic overlap
user1_langs = ["Python", "JavaScript", "Go"]
user2_langs = ["Python", "Java", "C++"]
shared = set(user1_langs) & set(user2_langs)
assert len(shared) == 1 and "Python" in shared, f"Expected 1 shared language (Python), got {shared}"

# Test case 2: No overlap
user3_langs = ["Rust", "Haskell"]
user4_langs = ["PHP", "Ruby"]
shared = set(user3_langs) & set(user4_langs)
assert len(shared) == 0, f"Expected no shared languages, got {shared}"

# Test case 3: Full overlap
user5_langs = ["Python", "JavaScript"]
user6_langs = ["Python", "JavaScript"]
shared = set(user5_langs) & set(user6_langs)
assert len(shared) == 2, f"Expected 2 shared languages, got {shared}"

print("✅ All shared languages calculation tests passed!")

def test_recommendation_sorting():
"""Test the recommendation sorting logic."""
print("Testing recommendation sorting...")

# Mock recommendation data
recommendations = [
{'shared_count': 1, 'karma': 50, 'name': 'User1'},
{'shared_count': 3, 'karma': 10, 'name': 'User2'},
{'shared_count': 2, 'karma': 100, 'name': 'User3'},
{'shared_count': 3, 'karma': 20, 'name': 'User4'},
]

# Sort by shared_count (descending), then by karma (descending)
recommendations.sort(key=lambda x: (-x['shared_count'], -x['karma']))

# Expected order: User4 (3,20), User2 (3,10), User3 (2,100), User1 (1,50)
expected_names = ['User4', 'User2', 'User3', 'User1']
actual_names = [rec['name'] for rec in recommendations]

assert actual_names == expected_names, f"Expected {expected_names}, got {actual_names}"

print("✅ Recommendation sorting test passed!")

def test_message_formatting():
"""Test basic message formatting logic."""
print("Testing message formatting...")

user_languages = ["Python", "JavaScript"]
user_languages_str = ", ".join(sorted(user_languages))

expected = "JavaScript, Python"
assert user_languages_str == expected, f"Expected '{expected}', got '{user_languages_str}'"

# Test shared languages formatting
shared_languages = ["Python", "Go"]
shared_languages_str = ", ".join(shared_languages)
expected_shared = "Python, Go"
assert shared_languages_str == expected_shared, f"Expected '{expected_shared}', got '{shared_languages_str}'"

print("✅ Message formatting test passed!")

def main():
"""Run all tests."""
print("🧪 Testing Friend Recommendations Feature")
print("=" * 50)

try:
test_shared_languages_calculation()
test_recommendation_sorting()
test_message_formatting()

print("=" * 50)
print("✅ All tests passed! The friend recommendations feature should work correctly.")
return 0

except AssertionError as e:
print(f"❌ Test failed: {e}")
return 1
except Exception as e:
print(f"❌ Unexpected error: {e}")
return 1

if __name__ == "__main__":
sys.exit(main())
1 change: 1 addition & 0 deletions python/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ def __init__(
(patterns.PEOPLE_LANGUAGES, self.commands.top_langs),
(patterns.BOTTOM_LANGUAGES,
lambda: self.commands.top_langs(True)),
(patterns.FRIENDS_RECOMMENDATIONS, self.commands.friends_recommendations),
(patterns.WHAT_IS, self.commands.what_is),
(patterns.WHAT_MEAN, self.commands.what_is),
(patterns.APPLY_KARMA, self.commands.apply_karma),
Expand Down
65 changes: 65 additions & 0 deletions python/modules/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,71 @@ def top_langs(
self.vk_instance.send_msg(built, self.peer_id)
return

def friends_recommendations(self) -> NoReturn:
"""Sends friend recommendations based on shared programming languages."""
if self.peer_id < 2e9:
return
maximum_users = self.matched.group("maximum_users")
maximum_users = int(maximum_users) if maximum_users else 10

# Get current user's programming languages
current_user_languages = self.data_service.get_user_sorted_programming_languages(self.current_user)
if not current_user_languages:
self.vk_instance.send_msg(
"Сначала добавьте свои языки программирования (например: += Python).",
self.peer_id
)
return

# Get all users with their programming languages
all_users = DataBuilder.get_users_sorted_by_karma(
self.vk_instance, self.data_service, self.peer_id)

# Calculate compatibility scores for each user
recommendations = []
current_user_id = self.current_user.uid

for user_data in all_users:
user_id = self.data_service.get_user_property(user_data, "uid")
# Skip current user
if user_id == current_user_id:
continue

user_languages = self.data_service.get_user_property(user_data, "programming_languages")
if not user_languages or not isinstance(user_languages, list):
continue

# Calculate number of shared languages
shared_languages = set(current_user_languages) & set(user_languages)
if shared_languages:
recommendations.append({
'user_data': user_data,
'shared_count': len(shared_languages),
'shared_languages': sorted(list(shared_languages))
})

# Sort by number of shared languages (descending), then by karma
recommendations.sort(key=lambda x: (
-x['shared_count'],
-self.data_service.get_user_property(x['user_data'], 'karma')
))

# Limit results
recommendations = recommendations[:maximum_users]

if not recommendations:
self.vk_instance.send_msg(
"Пока не найдено пользователей с общими языками программирования.",
self.peer_id
)
return

self.vk_instance.send_msg(
CommandsBuilder.build_friends_recommendations(
recommendations, self.data_service, current_user_languages),
self.peer_id
)

def apply_karma(self) -> NoReturn:
"""Changes user karma."""
if self.peer_id < 2e9 or not self.karma_enabled or not self.matched or self.is_bot_selected:
Expand Down
54 changes: 54 additions & 0 deletions python/modules/commands_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,3 +185,57 @@ def build_karma_change(
return ("Карма изменена: [id%s|%s] [%s]->[%s]. Голосовали: (%s)" %
(selected_user_karma_change + (", ".join([f"@id{voter}" for voter in voters]),)))
return None

@staticmethod
def build_friends_recommendations(
recommendations: List[dict],
data: BetterBotBaseDataService,
user_languages: List[str]
) -> str:
"""Builds friends recommendations message based on shared programming languages.

Arguments:
- {recommendations} - list of recommendation dicts with user_data, shared_count, shared_languages
- {data} - data service
- {user_languages} - current user's programming languages
"""
if not recommendations:
return "Пока не найдено пользователей с общими языками программирования."

user_languages_str = ", ".join(sorted(user_languages))
message_lines = [f"🤝 Рекомендации друзей (ваши языки: {user_languages_str}):\n"]

for i, rec in enumerate(recommendations, 1):
user_data = rec['user_data']
shared_count = rec['shared_count']
shared_languages = rec['shared_languages']

user_id = data.get_user_property(user_data, 'uid')
user_name = data.get_user_property(user_data, 'name')
karma = DataBuilder.build_karma(user_data, data)
github_profile = DataBuilder.build_github_profile(user_data, data, prefix=" - ", default="")

shared_languages_str = ", ".join(shared_languages)

# Format: "1. [id123|Name] (karma) - github.com/user - общие: Python, Java (2)"
line = (f"{i}. [id{user_id}|{user_name}] ({karma}){github_profile} - "
f"общие: {shared_languages_str} ({shared_count})")
message_lines.append(line)

# Check total message length and truncate if needed
full_message = '\n'.join(message_lines)
if len(full_message) > 4000: # Leave some margin for VK's 4096 character limit
# Truncate the recommendations and add notice
truncated_lines = message_lines[:1] # Keep header
current_length = len(message_lines[0])

for line in message_lines[1:]:
if current_length + len(line) + 50 > 4000: # 50 chars for truncation notice
truncated_lines.append("... (список сокращён)")
break
truncated_lines.append(line)
current_length += len(line) + 1 # +1 for newline

full_message = '\n'.join(truncated_lines)

return full_message
3 changes: 3 additions & 0 deletions python/patterns.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@
r'\A\s*(люди|народ|people)\s*(?P<languages>(' + DEFAULT_LANGUAGES +
r')(\s+(' + DEFAULT_LANGUAGES + r'))*)\s*\Z', IGNORECASE)

FRIENDS_RECOMMENDATIONS = recompile(
r'\A\s*(друзья|friends|рекомендации|recommendations)\s*(?P<maximum_users>\d+)?\s*\Z', IGNORECASE)

WHAT_IS = recompile(
r'\A\s*(what is|что такое|що таке)\s+(?P<question>[\S\s]+?)\??\s*\Z', IGNORECASE)

Expand Down
26 changes: 26 additions & 0 deletions python/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,32 @@ def test_apply_karma_change(
self.commands.apply_karma_change('-', 6)
self.commands.karma_message()

@ordered
def test_friends_recommendations(
self
) -> NoReturn:
"""Test friend recommendations feature."""
# Set up test data - create user 3 with some shared languages
user_3 = db.get_or_create_user(3, None)
user_3.programming_languages = ['Python', 'Java']
user_3.karma = 50
db.save_user(user_3)

# Set current user (user 2) to have some languages
self.commands.current_user = db.get_user(2)
self.commands.current_user.programming_languages = ['Python', 'JavaScript']
db.save_user(self.commands.current_user)

# Test the friends recommendations command
self.commands.msg = 'friends'
self.commands.match_command(patterns.FRIENDS_RECOMMENDATIONS)
self.commands.friends_recommendations()

# Test with maximum users limit
self.commands.msg = 'friends 5'
self.commands.match_command(patterns.FRIENDS_RECOMMENDATIONS)
self.commands.friends_recommendations()


if __name__ == '__main__':
db = BetterBotBaseDataService("test_db")
Expand Down
Loading