Spaces:
Sleeping
Sleeping
Commit ·
30e1fea
1
Parent(s): 1cdf020
10/4/2026
Browse files- app.py +4 -4
- app_SuEnv.py +4 -4
- inference.py +3 -3
- models.py +6 -6
- server/app.py +4 -4
- server/environment.py +2 -2
- server/graders.py +21 -20
- server/semantic_scorer.py +4 -4
app.py
CHANGED
|
@@ -47,11 +47,11 @@ MAX_SESSIONS = 100
|
|
| 47 |
# Global metrics tracking (shared)
|
| 48 |
GLOBAL_METRICS = {
|
| 49 |
"total_episodes": 0,
|
| 50 |
-
"success_rate": 0.
|
| 51 |
"total_successful": 0,
|
| 52 |
-
"avg_easy_score": 0.
|
| 53 |
-
"avg_medium_score": 0.
|
| 54 |
-
"avg_hard_score": 0.
|
| 55 |
"scores_by_difficulty": {"easy": [], "medium": [], "hard": []}
|
| 56 |
}
|
| 57 |
|
|
|
|
| 47 |
# Global metrics tracking (shared)
|
| 48 |
GLOBAL_METRICS = {
|
| 49 |
"total_episodes": 0,
|
| 50 |
+
"success_rate": 0.01,
|
| 51 |
"total_successful": 0,
|
| 52 |
+
"avg_easy_score": 0.01,
|
| 53 |
+
"avg_medium_score": 0.01,
|
| 54 |
+
"avg_hard_score": 0.01,
|
| 55 |
"scores_by_difficulty": {"easy": [], "medium": [], "hard": []}
|
| 56 |
}
|
| 57 |
|
app_SuEnv.py
CHANGED
|
@@ -36,11 +36,11 @@ active_sessions: Dict[str, SupportEnvironment] = {}
|
|
| 36 |
episode_history: List[Dict[str, Any]] = []
|
| 37 |
GLOBAL_METRICS = {
|
| 38 |
"total_episodes": 0,
|
| 39 |
-
"success_rate": 0.
|
| 40 |
"total_successful": 0,
|
| 41 |
-
"avg_easy_score": 0.
|
| 42 |
-
"avg_medium_score": 0.
|
| 43 |
-
"avg_hard_score": 0.
|
| 44 |
"scores_by_difficulty": {"easy": [], "medium": [], "hard": []}
|
| 45 |
}
|
| 46 |
|
|
|
|
| 36 |
episode_history: List[Dict[str, Any]] = []
|
| 37 |
GLOBAL_METRICS = {
|
| 38 |
"total_episodes": 0,
|
| 39 |
+
"success_rate": 0.01,
|
| 40 |
"total_successful": 0,
|
| 41 |
+
"avg_easy_score": 0.01,
|
| 42 |
+
"avg_medium_score": 0.01,
|
| 43 |
+
"avg_hard_score": 0.01,
|
| 44 |
"scores_by_difficulty": {"easy": [], "medium": [], "hard": []}
|
| 45 |
}
|
| 46 |
|
inference.py
CHANGED
|
@@ -156,7 +156,7 @@ def main() -> None:
|
|
| 156 |
rewards: List[float] = []
|
| 157 |
steps_taken = 0
|
| 158 |
success = False
|
| 159 |
-
final_score = 0.
|
| 160 |
|
| 161 |
try:
|
| 162 |
with env.sync() as conn:
|
|
@@ -212,7 +212,7 @@ def main() -> None:
|
|
| 212 |
grade_resp = requests.post(grader_url, json={"session_id": session_id}, timeout=30)
|
| 213 |
if grade_resp.status_code == 200:
|
| 214 |
grade_data = grade_resp.json()
|
| 215 |
-
final_score = float(grade_data.get('score', 0.
|
| 216 |
success = bool(grade_data.get('passed', False))
|
| 217 |
except Exception:
|
| 218 |
pass
|
|
@@ -220,7 +220,7 @@ def main() -> None:
|
|
| 220 |
log_end(success=success, steps=steps_taken, score=final_score, rewards=rewards)
|
| 221 |
|
| 222 |
except Exception as e:
|
| 223 |
-
log_end(success=False, steps=steps_taken, score=0.
|
| 224 |
|
| 225 |
if __name__ == "__main__":
|
| 226 |
main()
|
|
|
|
| 156 |
rewards: List[float] = []
|
| 157 |
steps_taken = 0
|
| 158 |
success = False
|
| 159 |
+
final_score = 0.01
|
| 160 |
|
| 161 |
try:
|
| 162 |
with env.sync() as conn:
|
|
|
|
| 212 |
grade_resp = requests.post(grader_url, json={"session_id": session_id}, timeout=30)
|
| 213 |
if grade_resp.status_code == 200:
|
| 214 |
grade_data = grade_resp.json()
|
| 215 |
+
final_score = float(grade_data.get('score', 0.01))
|
| 216 |
success = bool(grade_data.get('passed', False))
|
| 217 |
except Exception:
|
| 218 |
pass
|
|
|
|
| 220 |
log_end(success=success, steps=steps_taken, score=final_score, rewards=rewards)
|
| 221 |
|
| 222 |
except Exception as e:
|
| 223 |
+
log_end(success=False, steps=steps_taken, score=0.01, rewards=rewards)
|
| 224 |
|
| 225 |
if __name__ == "__main__":
|
| 226 |
main()
|
models.py
CHANGED
|
@@ -90,13 +90,13 @@ class SupportState(State):
|
|
| 90 |
|
| 91 |
# Performance tracking
|
| 92 |
classification_correct: bool = False
|
| 93 |
-
response_quality_score: float = 0.
|
| 94 |
escalation_correct: bool = False
|
| 95 |
resolved: bool = False
|
| 96 |
|
| 97 |
# Cumulative metrics
|
| 98 |
-
total_reward: float = 0.
|
| 99 |
-
customer_sentiment: float = 0.
|
| 100 |
|
| 101 |
|
| 102 |
class PublicSupportState(State):
|
|
@@ -111,10 +111,10 @@ class PublicSupportState(State):
|
|
| 111 |
|
| 112 |
# Performance tracking
|
| 113 |
classification_correct: bool = False
|
| 114 |
-
response_quality_score: float = 0.
|
| 115 |
escalation_correct: bool = False
|
| 116 |
resolved: bool = False
|
| 117 |
|
| 118 |
# Cumulative metrics
|
| 119 |
-
total_reward: float = 0.
|
| 120 |
-
customer_sentiment: float = 0.
|
|
|
|
| 90 |
|
| 91 |
# Performance tracking
|
| 92 |
classification_correct: bool = False
|
| 93 |
+
response_quality_score: float = 0.01
|
| 94 |
escalation_correct: bool = False
|
| 95 |
resolved: bool = False
|
| 96 |
|
| 97 |
# Cumulative metrics
|
| 98 |
+
total_reward: float = 0.01
|
| 99 |
+
customer_sentiment: float = 0.01
|
| 100 |
|
| 101 |
|
| 102 |
class PublicSupportState(State):
|
|
|
|
| 111 |
|
| 112 |
# Performance tracking
|
| 113 |
classification_correct: bool = False
|
| 114 |
+
response_quality_score: float = 0.01
|
| 115 |
escalation_correct: bool = False
|
| 116 |
resolved: bool = False
|
| 117 |
|
| 118 |
# Cumulative metrics
|
| 119 |
+
total_reward: float = 0.01
|
| 120 |
+
customer_sentiment: float = 0.01
|
server/app.py
CHANGED
|
@@ -56,11 +56,11 @@ SESSION_TTL_SECONDS = 3600
|
|
| 56 |
# Global metrics tracking
|
| 57 |
METRICS = {
|
| 58 |
"total_episodes": 0,
|
| 59 |
-
"success_rate": 0.
|
| 60 |
"total_successful": 0,
|
| 61 |
-
"avg_easy_score": 0.
|
| 62 |
-
"avg_medium_score": 0.
|
| 63 |
-
"avg_hard_score": 0.
|
| 64 |
"scores_by_difficulty": {"easy": [], "medium": [], "hard": []}
|
| 65 |
}
|
| 66 |
|
|
|
|
| 56 |
# Global metrics tracking
|
| 57 |
METRICS = {
|
| 58 |
"total_episodes": 0,
|
| 59 |
+
"success_rate": 0.01,
|
| 60 |
"total_successful": 0,
|
| 61 |
+
"avg_easy_score": 0.01,
|
| 62 |
+
"avg_medium_score": 0.01,
|
| 63 |
+
"avg_hard_score": 0.01,
|
| 64 |
"scores_by_difficulty": {"easy": [], "medium": [], "hard": []}
|
| 65 |
}
|
| 66 |
|
server/environment.py
CHANGED
|
@@ -107,10 +107,10 @@ class SupportEnvironment(Environment):
|
|
| 107 |
task_difficulty=difficulty,
|
| 108 |
max_steps=max_steps,
|
| 109 |
classification_correct=False,
|
| 110 |
-
response_quality_score=0.
|
| 111 |
escalation_correct=False,
|
| 112 |
resolved=False,
|
| 113 |
-
total_reward=0.
|
| 114 |
)
|
| 115 |
|
| 116 |
# Reset episode flags
|
|
|
|
| 107 |
task_difficulty=difficulty,
|
| 108 |
max_steps=max_steps,
|
| 109 |
classification_correct=False,
|
| 110 |
+
response_quality_score=0.01,
|
| 111 |
escalation_correct=False,
|
| 112 |
resolved=False,
|
| 113 |
+
total_reward=0.01
|
| 114 |
)
|
| 115 |
|
| 116 |
# Reset episode flags
|
server/graders.py
CHANGED
|
@@ -116,8 +116,9 @@ class SupportGrader:
|
|
| 116 |
total_score += ordering_penalty
|
| 117 |
|
| 118 |
# Ensure score is strictly within (0, 1) range per validation rules
|
| 119 |
-
#
|
| 120 |
-
total_score =
|
|
|
|
| 121 |
|
| 122 |
# Generate feedback
|
| 123 |
feedback = self._generate_feedback(breakdown, task_difficulty)
|
|
@@ -126,8 +127,8 @@ class SupportGrader:
|
|
| 126 |
passed = total_score >= 0.6
|
| 127 |
|
| 128 |
return GradeResult(
|
| 129 |
-
score=
|
| 130 |
-
breakdown={k:
|
| 131 |
feedback=feedback,
|
| 132 |
passed=passed
|
| 133 |
)
|
|
@@ -146,7 +147,7 @@ class SupportGrader:
|
|
| 146 |
if idx_resolve_or_escalate != -1:
|
| 147 |
if idx_classify == -1 or idx_resolve_or_escalate < idx_classify:
|
| 148 |
return -0.25 # Penalty
|
| 149 |
-
return 0.
|
| 150 |
|
| 151 |
def _grade_classification(
|
| 152 |
self,
|
|
@@ -159,14 +160,14 @@ class SupportGrader:
|
|
| 159 |
]
|
| 160 |
|
| 161 |
if not classifications:
|
| 162 |
-
return 0.
|
| 163 |
|
| 164 |
# Check first classification (most important)
|
| 165 |
first_class = classifications[0].get("content", "").lower().strip()
|
| 166 |
target_clean = target.lower().strip()
|
| 167 |
|
| 168 |
if first_class == target_clean:
|
| 169 |
-
return
|
| 170 |
|
| 171 |
# Partial credit for related categories
|
| 172 |
category_relations = {
|
|
@@ -186,7 +187,7 @@ class SupportGrader:
|
|
| 186 |
if cls.get("content", "").lower().strip() == target_clean:
|
| 187 |
return 0.5 # Corrected but not first try
|
| 188 |
|
| 189 |
-
return 0.
|
| 190 |
|
| 191 |
def _grade_responses(
|
| 192 |
self,
|
|
@@ -200,7 +201,7 @@ class SupportGrader:
|
|
| 200 |
]
|
| 201 |
|
| 202 |
if not responses:
|
| 203 |
-
return 0.
|
| 204 |
|
| 205 |
total_score = 0.0
|
| 206 |
|
|
@@ -227,7 +228,7 @@ class SupportGrader:
|
|
| 227 |
|
| 228 |
# Response length check
|
| 229 |
word_count = len(content.split())
|
| 230 |
-
length_score = min(
|
| 231 |
|
| 232 |
# ANTI-GAMING: Detect keyword stuffing
|
| 233 |
# Stuffing = many keywords in short text (high density), not just many keywords
|
|
@@ -264,7 +265,7 @@ class SupportGrader:
|
|
| 264 |
# If confidence is < 0.5, it's a weak response
|
| 265 |
resp_score *= (0.5 + 0.5 * confidence)
|
| 266 |
|
| 267 |
-
total_score += max(0.
|
| 268 |
|
| 269 |
# Average across responses
|
| 270 |
avg_score = total_score / len(responses)
|
|
@@ -283,7 +284,7 @@ class SupportGrader:
|
|
| 283 |
# easy → no adjustment
|
| 284 |
# medium → scores below 0.6 are compressed (×0.85 multiplier)
|
| 285 |
# hard → scores below 0.7 are compressed (×0.75 multiplier)
|
| 286 |
-
# A perfect raw score still maps to
|
| 287 |
if difficulty == "hard":
|
| 288 |
if avg_score < 0.7:
|
| 289 |
avg_score *= 0.75 # weak response on a hard task
|
|
@@ -292,7 +293,7 @@ class SupportGrader:
|
|
| 292 |
if avg_score < 0.6:
|
| 293 |
avg_score *= 0.85 # weak response on a medium task
|
| 294 |
|
| 295 |
-
return max(0.
|
| 296 |
|
| 297 |
def _grade_escalation(
|
| 298 |
self,
|
|
@@ -333,18 +334,18 @@ class SupportGrader:
|
|
| 333 |
# Check reason quality
|
| 334 |
reason = escalations[0].get("content", "")
|
| 335 |
if len(reason.split()) >= 10 and any(kw in reason.lower() for kw in ["immediate", "severity", "sensitivity", "human"]):
|
| 336 |
-
return
|
| 337 |
elif len(reason.split()) >= 5:
|
| 338 |
return 0.9
|
| 339 |
return 0.7 # Right decision but poor explanation
|
| 340 |
|
| 341 |
elif not should_escalate and not escalated:
|
| 342 |
# Correct: did not escalate when not needed
|
| 343 |
-
return
|
| 344 |
|
| 345 |
elif should_escalate and not escalated:
|
| 346 |
# Wrong: should have escalated but didn't
|
| 347 |
-
return 0.
|
| 348 |
|
| 349 |
else:
|
| 350 |
# Wrong: escalated when not needed (unnecessary escalation)
|
|
@@ -408,12 +409,12 @@ class SupportGrader:
|
|
| 408 |
base_score = overlap_ratio * 0.7
|
| 409 |
action_bonus = 0.3 if has_action_word else 0.0
|
| 410 |
|
| 411 |
-
return min(
|
| 412 |
|
| 413 |
def _grade_efficiency(self, steps: int, max_steps: int, difficulty: str = "easy") -> float:
|
| 414 |
"""Grade step efficiency. Harder tasks require more deliberation."""
|
| 415 |
if steps <= 1:
|
| 416 |
-
return
|
| 417 |
|
| 418 |
# Stricter for hard: optimal path is usually 4-6 steps
|
| 419 |
if difficulty == "hard":
|
|
@@ -421,12 +422,12 @@ class SupportGrader:
|
|
| 421 |
if steps < 5:
|
| 422 |
return 0.4 # Superficial handling
|
| 423 |
if steps <= 9:
|
| 424 |
-
return
|
| 425 |
# Penalize the tail end for inefficiency
|
| 426 |
return round(max(0.3, 1.0 - 0.8 * ((steps - 9) / (max_steps - 9))), 2)
|
| 427 |
|
| 428 |
# Standard linear for easy/medium
|
| 429 |
-
return round(
|
| 430 |
|
| 431 |
def _get_weights(self, difficulty: str) -> Dict[str, float]:
|
| 432 |
"""Get grading weights based on difficulty."""
|
|
|
|
| 116 |
total_score += ordering_penalty
|
| 117 |
|
| 118 |
# Ensure score is strictly within (0, 1) range per validation rules
|
| 119 |
+
# We round and then clamp to [0.01, 0.99] to guarantee it never hits 0.0 or 1.0
|
| 120 |
+
total_score = round(total_score, 4)
|
| 121 |
+
total_score = max(0.01, min(0.99, total_score))
|
| 122 |
|
| 123 |
# Generate feedback
|
| 124 |
feedback = self._generate_feedback(breakdown, task_difficulty)
|
|
|
|
| 127 |
passed = total_score >= 0.6
|
| 128 |
|
| 129 |
return GradeResult(
|
| 130 |
+
score=total_score,
|
| 131 |
+
breakdown={k: max(0.01, min(0.99, round(v, 4))) for k, v in breakdown.items()},
|
| 132 |
feedback=feedback,
|
| 133 |
passed=passed
|
| 134 |
)
|
|
|
|
| 147 |
if idx_resolve_or_escalate != -1:
|
| 148 |
if idx_classify == -1 or idx_resolve_or_escalate < idx_classify:
|
| 149 |
return -0.25 # Penalty
|
| 150 |
+
return 0.01
|
| 151 |
|
| 152 |
def _grade_classification(
|
| 153 |
self,
|
|
|
|
| 160 |
]
|
| 161 |
|
| 162 |
if not classifications:
|
| 163 |
+
return 0.01 # No classification attempted
|
| 164 |
|
| 165 |
# Check first classification (most important)
|
| 166 |
first_class = classifications[0].get("content", "").lower().strip()
|
| 167 |
target_clean = target.lower().strip()
|
| 168 |
|
| 169 |
if first_class == target_clean:
|
| 170 |
+
return 0.99
|
| 171 |
|
| 172 |
# Partial credit for related categories
|
| 173 |
category_relations = {
|
|
|
|
| 187 |
if cls.get("content", "").lower().strip() == target_clean:
|
| 188 |
return 0.5 # Corrected but not first try
|
| 189 |
|
| 190 |
+
return 0.21 # Wrong classification
|
| 191 |
|
| 192 |
def _grade_responses(
|
| 193 |
self,
|
|
|
|
| 201 |
]
|
| 202 |
|
| 203 |
if not responses:
|
| 204 |
+
return 0.01
|
| 205 |
|
| 206 |
total_score = 0.0
|
| 207 |
|
|
|
|
| 228 |
|
| 229 |
# Response length check
|
| 230 |
word_count = len(content.split())
|
| 231 |
+
length_score = min(0.99, word_count / 20) # At least 20 words ideal
|
| 232 |
|
| 233 |
# ANTI-GAMING: Detect keyword stuffing
|
| 234 |
# Stuffing = many keywords in short text (high density), not just many keywords
|
|
|
|
| 265 |
# If confidence is < 0.5, it's a weak response
|
| 266 |
resp_score *= (0.5 + 0.5 * confidence)
|
| 267 |
|
| 268 |
+
total_score += max(0.01, min(0.99, resp_score))
|
| 269 |
|
| 270 |
# Average across responses
|
| 271 |
avg_score = total_score / len(responses)
|
|
|
|
| 284 |
# easy → no adjustment
|
| 285 |
# medium → scores below 0.6 are compressed (×0.85 multiplier)
|
| 286 |
# hard → scores below 0.7 are compressed (×0.75 multiplier)
|
| 287 |
+
# A perfect raw score still maps to 0.99 at every difficulty.
|
| 288 |
if difficulty == "hard":
|
| 289 |
if avg_score < 0.7:
|
| 290 |
avg_score *= 0.75 # weak response on a hard task
|
|
|
|
| 293 |
if avg_score < 0.6:
|
| 294 |
avg_score *= 0.85 # weak response on a medium task
|
| 295 |
|
| 296 |
+
return max(0.01, min(0.99, avg_score))
|
| 297 |
|
| 298 |
def _grade_escalation(
|
| 299 |
self,
|
|
|
|
| 334 |
# Check reason quality
|
| 335 |
reason = escalations[0].get("content", "")
|
| 336 |
if len(reason.split()) >= 10 and any(kw in reason.lower() for kw in ["immediate", "severity", "sensitivity", "human"]):
|
| 337 |
+
return 0.99
|
| 338 |
elif len(reason.split()) >= 5:
|
| 339 |
return 0.9
|
| 340 |
return 0.7 # Right decision but poor explanation
|
| 341 |
|
| 342 |
elif not should_escalate and not escalated:
|
| 343 |
# Correct: did not escalate when not needed
|
| 344 |
+
return 0.99
|
| 345 |
|
| 346 |
elif should_escalate and not escalated:
|
| 347 |
# Wrong: should have escalated but didn't
|
| 348 |
+
return 0.01 # Harsher penalty for missing required escalation
|
| 349 |
|
| 350 |
else:
|
| 351 |
# Wrong: escalated when not needed (unnecessary escalation)
|
|
|
|
| 409 |
base_score = overlap_ratio * 0.7
|
| 410 |
action_bonus = 0.3 if has_action_word else 0.0
|
| 411 |
|
| 412 |
+
return min(0.99, base_score + action_bonus)
|
| 413 |
|
| 414 |
def _grade_efficiency(self, steps: int, max_steps: int, difficulty: str = "easy") -> float:
|
| 415 |
"""Grade step efficiency. Harder tasks require more deliberation."""
|
| 416 |
if steps <= 1:
|
| 417 |
+
return 0.99 if difficulty == "easy" else 0.5 # Discourage one-step solutions for complex tasks
|
| 418 |
|
| 419 |
# Stricter for hard: optimal path is usually 4-6 steps
|
| 420 |
if difficulty == "hard":
|
|
|
|
| 422 |
if steps < 5:
|
| 423 |
return 0.4 # Superficial handling
|
| 424 |
if steps <= 9:
|
| 425 |
+
return 0.99 # High quality deliberation
|
| 426 |
# Penalize the tail end for inefficiency
|
| 427 |
return round(max(0.3, 1.0 - 0.8 * ((steps - 9) / (max_steps - 9))), 2)
|
| 428 |
|
| 429 |
# Standard linear for easy/medium
|
| 430 |
+
return round(0.99 - 0.8 * ((steps - 1) / (max_steps - 1)), 2)
|
| 431 |
|
| 432 |
def _get_weights(self, difficulty: str) -> Dict[str, float]:
|
| 433 |
"""Get grading weights based on difficulty."""
|
server/semantic_scorer.py
CHANGED
|
@@ -45,14 +45,14 @@ class SemanticScorer:
|
|
| 45 |
Returns a dict with empathy, solution, and resolution alignment scores.
|
| 46 |
"""
|
| 47 |
if not responses or not expected_resolution:
|
| 48 |
-
return {"empathy": 0.
|
| 49 |
|
| 50 |
if self.model is None:
|
| 51 |
return None
|
| 52 |
|
| 53 |
combined_response = " ".join([r for r in responses if len(r) > 10])
|
| 54 |
if not combined_response:
|
| 55 |
-
return {"empathy": 0.
|
| 56 |
|
| 57 |
try:
|
| 58 |
from sklearn.metrics.pairwise import cosine_similarity
|
|
@@ -73,11 +73,11 @@ class SemanticScorer:
|
|
| 73 |
|
| 74 |
# Non-linear scaling: 0.2 similarity is baseline, 0.9 similarity is perfect.
|
| 75 |
def scale(sim):
|
| 76 |
-
return min(0.
|
| 77 |
|
| 78 |
empathy_score = scale(sim_empathy)
|
| 79 |
solution_score = scale(sim_solution)
|
| 80 |
-
resolution_score = max(0.
|
| 81 |
|
| 82 |
overall = (empathy_score * 0.2) + (solution_score * 0.2) + (resolution_score * 0.6)
|
| 83 |
|
|
|
|
| 45 |
Returns a dict with empathy, solution, and resolution alignment scores.
|
| 46 |
"""
|
| 47 |
if not responses or not expected_resolution:
|
| 48 |
+
return {"empathy": 0.01, "solution": 0.01, "resolution": 0.01, "overall": 0.01}
|
| 49 |
|
| 50 |
if self.model is None:
|
| 51 |
return None
|
| 52 |
|
| 53 |
combined_response = " ".join([r for r in responses if len(r) > 10])
|
| 54 |
if not combined_response:
|
| 55 |
+
return {"empathy": 0.01, "solution": 0.01, "resolution": 0.01, "overall": 0.01}
|
| 56 |
|
| 57 |
try:
|
| 58 |
from sklearn.metrics.pairwise import cosine_similarity
|
|
|
|
| 73 |
|
| 74 |
# Non-linear scaling: 0.2 similarity is baseline, 0.9 similarity is perfect.
|
| 75 |
def scale(sim):
|
| 76 |
+
return min(0.99, max(0.01, (sim - 0.1) / 0.7))
|
| 77 |
|
| 78 |
empathy_score = scale(sim_empathy)
|
| 79 |
solution_score = scale(sim_solution)
|
| 80 |
+
resolution_score = max(0.01, min(0.99, (sim_resolution - 0.2) / 0.7))
|
| 81 |
|
| 82 |
overall = (empathy_score * 0.2) + (solution_score * 0.2) + (resolution_score * 0.6)
|
| 83 |
|