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
51 changes: 29 additions & 22 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"""
import pytest
import asyncio
from types import SimpleNamespace
from typing import Dict, Any, Optional
from unittest.mock import Mock, AsyncMock, MagicMock
from datetime import datetime
Expand Down Expand Up @@ -114,20 +115,23 @@ def mock_database_manager():
manager.get_chat_message_detail = AsyncMock(return_value=None)
manager.delete_chat_message = AsyncMock(return_value=True)

# Metrics methods
manager.get_diversity_metrics = AsyncMock(return_value={
'vocabulary_diversity': 0.5,
'topic_diversity': 0.6,
'style_diversity': 0.7,
'total_score': 0.6
})
manager.get_affection_metrics = AsyncMock(return_value={
'average_affection': 50,
'total_users': 10,
'high_affection_count': 3,
'low_affection_count': 2,
'distribution': []
# Metrics methods (real API used by MetricsService after refactor)
manager.get_detailed_metrics = AsyncMock(return_value={
'messages': {'raw': 100, 'filtered': 40, 'bot': 10},
'learning': {
'persona_reviews': 4,
'style_reviews': 3,
'batches': 2,
'style_patterns': 6,
},
'group_id': 'default',
})
manager.get_all_user_affections = AsyncMock(return_value=[
{'user_id': 'u1', 'affection_level': 80},
{'user_id': 'u2', 'affection_level': 50},
{'user_id': 'u3', 'affection_level': 20},
])
manager.get_total_affection = AsyncMock(return_value=150)

return manager

Expand Down Expand Up @@ -178,15 +182,18 @@ def mock_intelligence_metrics_service():
"""Mock IntelligenceMetricsService"""
service = AsyncMock()

service.calculate_metrics = AsyncMock(return_value={
'overall_score': 75,
'dimensions': {
'coherence': 80,
'relevance': 75,
'creativity': 70
},
'trends': []
})
# Real API: returns a LearningEfficiencyMetrics-like object
service.calculate_learning_efficiency = AsyncMock(return_value=SimpleNamespace(
overall_efficiency=72.5,
message_filter_rate=40.0,
content_refine_quality=0.0,
style_learning_progress=60.0,
persona_update_quality=50.0,
jargon_learning_score=0.0,
social_relation_score=0.0,
affection_score=30.0,
active_strategies_count=0,
))

return service

Expand Down
118 changes: 118 additions & 0 deletions tests/integration/test_metrics_service_endpoints.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
"""Integration tests for MetricsService-backed metrics endpoints.

These guard against the regression where MetricsService called methods that no
longer existed after the architecture refactor (calculate_metrics /
get_diversity_metrics / get_affection_metrics), making the endpoints always
return zeroed fallback data with a logged AttributeError.
"""

from types import SimpleNamespace

import pytest
from quart import Quart

import webui.blueprints.metrics as metrics_module
from webui.blueprints.metrics import metrics_bp


class DummyDatabaseManager:
async def get_detailed_metrics(self, group_id=None):
return {
"messages": {"raw": 100, "filtered": 40, "bot": 10},
"learning": {
"persona_reviews": 4,
"style_reviews": 3,
"batches": 2,
"style_patterns": 6,
},
"group_id": group_id,
}

async def get_all_user_affections(self, group_id):
return [
{"user_id": "u1", "affection_level": 80},
{"user_id": "u2", "affection_level": 50},
{"user_id": "u3", "affection_level": 20},
]


class DummyIntelligenceMetricsService:
async def calculate_learning_efficiency(self, **kwargs):
self.last_kwargs = kwargs
return SimpleNamespace(
overall_efficiency=72.5,
message_filter_rate=40.0,
content_refine_quality=0.0,
style_learning_progress=60.0,
persona_update_quality=50.0,
jargon_learning_score=0.0,
social_relation_score=0.0,
affection_score=30.0,
active_strategies_count=0,
)


@pytest.fixture
async def app(monkeypatch):
app = Quart(__name__)
app.config["TESTING"] = True
app.secret_key = "test-secret-key"

container = SimpleNamespace(
database_manager=DummyDatabaseManager(),
intelligence_metrics_service=DummyIntelligenceMetricsService(),
)
monkeypatch.setattr(metrics_module, "get_container", lambda: container)

app.register_blueprint(metrics_bp)
yield app


@pytest.fixture
async def client(app):
return app.test_client()


@pytest.mark.asyncio
async def test_intelligence_metrics_uses_learning_efficiency(client):
response = await client.get("/api/intelligence_metrics?group_id=g1")

assert response.status_code == 200
data = await response.get_json()

assert "error" not in data
assert data["overall_score"] == 72.5
assert data["dimensions"]["message_filter_rate"] == 40.0
assert data["dimensions"]["affection_score"] == 30.0
assert data["dimensions"]["active_strategies_count"] == 0
assert data["trends"] == []


@pytest.mark.asyncio
async def test_affection_metrics_computed_from_user_affections(client):
response = await client.get("/api/affection_metrics?group_id=g1")

assert response.status_code == 200
data = await response.get_json()

assert "error" not in data
assert data["total_users"] == 3
assert data["average_affection"] == 50.0
assert data["high_affection_count"] == 1 # level 80 >= 70
assert data["low_affection_count"] == 1 # level 20 <= 30
# buckets partition every user exactly once
assert sum(b["count"] for b in data["distribution"]) == 3


@pytest.mark.asyncio
async def test_diversity_metrics_estimated_from_style_patterns(client):
response = await client.get("/api/diversity_metrics?group_id=g1")

assert response.status_code == 200
data = await response.get_json()

assert "error" not in data
assert data["style_diversity"] == 12 # 6 patterns * 2
assert data["vocabulary_diversity"] == 0
assert data["topic_diversity"] == 0
assert data["total_score"] == 4.0 # (12 + 0 + 0) / 3
122 changes: 104 additions & 18 deletions webui/services/metrics_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,54 @@ async def get_intelligence_metrics(self, group_id: str = 'default') -> Dict[str,
}

try:
metrics = await self.intelligence_metrics_service.calculate_metrics(group_id)
return metrics if metrics else {
'overall_score': 0,
'dimensions': {},
'trends': []
# 从数据库收集学习效率计算所需的输入指标
total_messages = 0
filtered_messages = 0
style_patterns = 0
persona_updates = 0
affection_users = 0

db = self.database_manager
if db:
try:
detailed = await db.get_detailed_metrics(group_id)
if isinstance(detailed, dict):
msgs = detailed.get('messages', {}) or {}
learning = detailed.get('learning', {}) or {}
total_messages = int(msgs.get('raw', 0) or 0)
filtered_messages = int(msgs.get('filtered', 0) or 0)
style_patterns = int(learning.get('style_patterns', 0) or 0)
persona_updates = int(learning.get('persona_reviews', 0) or 0)
except Exception as e:
logger.warning(f"获取详细指标失败: {e}")

try:
affections = await db.get_all_user_affections(group_id)
affection_users = len(affections) if affections else 0
except Exception as e:
logger.warning(f"获取好感度用户数失败: {e}")

metrics = await self.intelligence_metrics_service.calculate_learning_efficiency(
total_messages=total_messages,
filtered_messages=filtered_messages,
style_patterns_learned=style_patterns,
persona_updates_count=persona_updates,
affection_users_count=affection_users,
)

return {
'overall_score': round(metrics.overall_efficiency, 1),
'dimensions': {
'message_filter_rate': round(metrics.message_filter_rate, 1),
'content_refine_quality': round(metrics.content_refine_quality, 1),
'style_learning_progress': round(metrics.style_learning_progress, 1),
'persona_update_quality': round(metrics.persona_update_quality, 1),
'jargon_learning_score': round(metrics.jargon_learning_score, 1),
'social_relation_score': round(metrics.social_relation_score, 1),
'affection_score': round(metrics.affection_score, 1),
'active_strategies_count': metrics.active_strategies_count,
},
'trends': [],
}
except Exception as e:
logger.error(f"获取智能指标失败: {e}", exc_info=True)
Expand Down Expand Up @@ -73,12 +116,25 @@ async def get_diversity_metrics(self, group_id: str = 'default') -> Dict[str, An
}

try:
diversity = await self.database_manager.get_diversity_metrics(group_id)
return diversity if diversity else {
'vocabulary_diversity': 0,
'topic_diversity': 0,
'style_diversity': 0,
'total_score': 0
# 当前架构没有独立的多样性数据源,基于已学到的风格模式做近似估算
style_patterns = 0
detailed = await self.database_manager.get_detailed_metrics(group_id)
if isinstance(detailed, dict):
learning = detailed.get('learning', {}) or {}
style_patterns = int(learning.get('style_patterns', 0) or 0)

style_diversity = min(style_patterns * 2, 100)
vocabulary_diversity = 0
topic_diversity = 0
total_score = round(
(style_diversity + vocabulary_diversity + topic_diversity) / 3, 1
)

return {
'vocabulary_diversity': vocabulary_diversity,
'topic_diversity': topic_diversity,
'style_diversity': style_diversity,
'total_score': total_score,
}
except Exception as e:
logger.error(f"获取多样性指标失败: {e}", exc_info=True)
Expand Down Expand Up @@ -110,13 +166,43 @@ async def get_affection_metrics(self, group_id: str = 'default') -> Dict[str, An
}

try:
affection = await self.database_manager.get_affection_metrics(group_id)
return affection if affection else {
'average_affection': 0,
'total_users': 0,
'high_affection_count': 0,
'low_affection_count': 0,
'distribution': []
affections = await self.database_manager.get_all_user_affections(group_id)
affections = affections or []

levels = []
for item in affections:
try:
levels.append(int(item.get('affection_level', 0) or 0))
except (TypeError, ValueError):
continue

total_users = len(levels)
average_affection = round(sum(levels) / total_users, 1) if total_users else 0
high_affection_count = sum(1 for lvl in levels if lvl >= 70)
low_affection_count = sum(1 for lvl in levels if lvl <= 30)

# 好感度分布 (0-20, 21-40, 41-60, 61-80, 81-100)
buckets = [
('0-20', 0, 20),
('21-40', 21, 40),
('41-60', 41, 60),
('61-80', 61, 80),
('81-100', 81, 100),
]
distribution = [
{
'range': label,
'count': sum(1 for lvl in levels if low <= lvl <= high),
}
for label, low, high in buckets
]

return {
'average_affection': average_affection,
'total_users': total_users,
'high_affection_count': high_affection_count,
'low_affection_count': low_affection_count,
'distribution': distribution,
}
except Exception as e:
logger.error(f"获取好感度指标失败: {e}", exc_info=True)
Expand Down