feat: IMAP mail sync with thread-to-contact matching (PRD §6.1)
Real IMAP connection via imapflow library: - Connects to IMAP server (IMAPFlow, TLS on port 993) - Fetches recent messages (configurable limit, default 20) - Extracts: from, fromEmail, subject, date, snippet (200 chars) - Thread-to-contact matching (PRD §6.1): 1. Match sender email against persons table 2. If no person match, extract domain and match against company.website 3. Returns matchedCompany + matchedPerson for each email - Audit-logged (action: crm.mailsync:imap) - Zod validated (host, port, user, password, folder, limit) - 502 on IMAP connection failure with detail - 400 on validation failure Tested: invalid creds → 502 'IMAP connection failed', missing host → 400
This commit is contained in:
1 parent
7acc0c5451
commit
e45b68bb8b
1089 files changed
+260614
No files matched your search
+78
@@ -0,0 +1,78 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .models import Question, AnswerResult
|
||||
|
||||
|
||||
class Evaluator:
|
||||
"""Evaluates user answers against correct answers.
|
||||
|
||||
- single_choice / true_false: exact match
|
||||
- fill_blank: case-insensitive, strip whitespace
|
||||
- short_answer: no auto-scoring, returns needs_review=True
|
||||
"""
|
||||
|
||||
def evaluate_answers(
|
||||
self,
|
||||
questions: list[Question],
|
||||
user_answers: dict[str, str],
|
||||
) -> list[AnswerResult]:
|
||||
results: list[AnswerResult] = []
|
||||
|
||||
for q in questions:
|
||||
user_answer = user_answers.get(q.id)
|
||||
|
||||
if q.type == "short_answer":
|
||||
# Short answer: cannot auto-evaluate, needs human/LLM review
|
||||
results.append(
|
||||
AnswerResult(
|
||||
question_id=q.id,
|
||||
user_answer=user_answer,
|
||||
is_correct=None,
|
||||
score=None,
|
||||
feedback="需要人工/LLM评判",
|
||||
error_type="pending_review",
|
||||
needs_review=True,
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
if user_answer is None:
|
||||
results.append(
|
||||
AnswerResult(
|
||||
question_id=q.id,
|
||||
user_answer=None,
|
||||
is_correct=False,
|
||||
score=0.0,
|
||||
feedback=f"未作答。正确答案:{q.answer}。{q.explanation}",
|
||||
error_type="no_answer",
|
||||
needs_review=False,
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
if q.type == "fill_blank":
|
||||
# Fill-in-the-blank: case-insensitive, strip whitespace
|
||||
is_correct = (
|
||||
user_answer.strip().lower() == str(q.answer).strip().lower()
|
||||
)
|
||||
else:
|
||||
# single_choice / true_false: exact match
|
||||
is_correct = user_answer.strip() == str(q.answer).strip()
|
||||
|
||||
results.append(
|
||||
AnswerResult(
|
||||
question_id=q.id,
|
||||
user_answer=user_answer,
|
||||
is_correct=is_correct,
|
||||
score=1.0 if is_correct else 0.0,
|
||||
feedback=(
|
||||
q.explanation
|
||||
if is_correct
|
||||
else f"回答错误。正确答案:{q.answer}。{q.explanation}"
|
||||
),
|
||||
error_type="none" if is_correct else "concept_confusion",
|
||||
needs_review=False,
|
||||
)
|
||||
)
|
||||
|
||||
return results
|
||||
Reference in new issue
Block a user