Spaces:
Sleeping
Sleeping
Commit ·
8374980
1
Parent(s): 5837391
v3.1
Browse files
backend/services/interview_service.py
CHANGED
|
@@ -44,7 +44,7 @@ TOPIC_INITIAL_DB_QUESTIONS = 5
|
|
| 44 |
TOPIC_INITIAL_ASK_COUNT = 4
|
| 45 |
TOPIC_AI_FOLLOWUPS = 3
|
| 46 |
TOPIC_DB_FOLLOWUPS = 2
|
| 47 |
-
TOPIC_TOTAL_QUESTIONS =
|
| 48 |
|
| 49 |
# Local process memory summary requested in workflow.
|
| 50 |
_LOCAL_SUMMARIES: dict[str, str] = {}
|
|
@@ -263,6 +263,21 @@ def _build_resume_resilient_followup_question(session: dict, question_number: in
|
|
| 263 |
return template.format(n=question_number, skill=skill, role=role_title)
|
| 264 |
|
| 265 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 266 |
async def _enqueue_resume_followup_with_fallback(
|
| 267 |
*,
|
| 268 |
redis,
|
|
@@ -1569,27 +1584,47 @@ async def _post_submit_topic_processing(
|
|
| 1569 |
session = await redis.hgetall(f"session:{session_id}")
|
| 1570 |
if not session:
|
| 1571 |
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1572 |
if session.get("topic_followups_generated", "0") == "1":
|
| 1573 |
return
|
| 1574 |
|
| 1575 |
qa_pairs = await get_session_qa(session_id)
|
| 1576 |
excluded_questions = await _get_session_question_texts(redis, session_id)
|
| 1577 |
|
|
|
|
| 1578 |
ai_items = await generate_topic_followup_batch(
|
| 1579 |
topic_name=session.get("role_title", "Topic Interview"),
|
| 1580 |
qa_pairs=qa_pairs,
|
| 1581 |
excluded_questions=excluded_questions,
|
| 1582 |
-
count=
|
| 1583 |
)
|
|
|
|
| 1584 |
db_items = await _sample_topic_questions(
|
| 1585 |
db=db,
|
| 1586 |
topic_id=session.get("topic_id", ""),
|
| 1587 |
excluded_questions=excluded_questions + [i.get("question", "") for i in ai_items],
|
| 1588 |
-
limit=
|
| 1589 |
)
|
| 1590 |
|
| 1591 |
topic_added = 0
|
| 1592 |
-
|
|
|
|
|
|
|
| 1593 |
qid = await enqueue_question(
|
| 1594 |
redis=redis,
|
| 1595 |
session_id=session_id,
|
|
@@ -1601,23 +1636,69 @@ async def _post_submit_topic_processing(
|
|
| 1601 |
)
|
| 1602 |
if qid:
|
| 1603 |
topic_added += 1
|
|
|
|
| 1604 |
|
| 1605 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1606 |
await _apply_generation_metric_delta(
|
| 1607 |
db=db,
|
| 1608 |
redis=redis,
|
| 1609 |
session_id=session_id,
|
| 1610 |
session=session,
|
| 1611 |
metrics_delta={
|
| 1612 |
-
"gemini_calls": 1,
|
| 1613 |
-
"gemini_questions":
|
| 1614 |
-
"bank_questions":
|
| 1615 |
-
"bank_shortfall": 0,
|
| 1616 |
"generation_batches": 1,
|
| 1617 |
},
|
| 1618 |
generated_count=generated_count,
|
| 1619 |
-
extra_redis_fields={
|
| 1620 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1621 |
)
|
| 1622 |
|
| 1623 |
await flush_backlog_to_queue(
|
|
@@ -1717,6 +1798,14 @@ async def submit_answer(session_id: str, question_id: str, answer: str) -> dict:
|
|
| 1717 |
{"$set": {"max_questions": max_questions}},
|
| 1718 |
)
|
| 1719 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1720 |
_update_local_summary(session_id, current_question_text, answer)
|
| 1721 |
await push_context_item(
|
| 1722 |
redis=redis,
|
|
@@ -1827,6 +1916,25 @@ async def submit_answer(session_id: str, question_id: str, answer: str) -> dict:
|
|
| 1827 |
)
|
| 1828 |
next_question_id, q_data = await pop_next_question(redis, session_id)
|
| 1829 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1830 |
if not next_question_id or not q_data:
|
| 1831 |
await redis.hset(
|
| 1832 |
f"session:{session_id}",
|
|
|
|
| 44 |
TOPIC_INITIAL_ASK_COUNT = 4
|
| 45 |
TOPIC_AI_FOLLOWUPS = 3
|
| 46 |
TOPIC_DB_FOLLOWUPS = 2
|
| 47 |
+
TOPIC_TOTAL_QUESTIONS = 10
|
| 48 |
|
| 49 |
# Local process memory summary requested in workflow.
|
| 50 |
_LOCAL_SUMMARIES: dict[str, str] = {}
|
|
|
|
| 263 |
return template.format(n=question_number, skill=skill, role=role_title)
|
| 264 |
|
| 265 |
|
| 266 |
+
def _build_topic_resilient_followup_question(session: dict, question_number: int, variant: int = 0) -> str:
|
| 267 |
+
topic_name = (session.get("role_title") or "this topic").strip()
|
| 268 |
+
index = max(0, question_number - 1) + max(0, variant)
|
| 269 |
+
|
| 270 |
+
templates = [
|
| 271 |
+
"Question {n}: Explain {topic} with a practical example from a production-like scenario.",
|
| 272 |
+
"Question {n}: What are the most common failure patterns in {topic}, and how would you detect them early?",
|
| 273 |
+
"Question {n}: Design a step-by-step implementation plan for {topic} with measurable checkpoints.",
|
| 274 |
+
"Question {n}: Compare two approaches in {topic}, including trade-offs in scalability, latency, and maintainability.",
|
| 275 |
+
"Question {n}: If a {topic} solution regressed after deployment, how would you triage and recover safely?",
|
| 276 |
+
]
|
| 277 |
+
template = templates[index % len(templates)]
|
| 278 |
+
return template.format(n=question_number, topic=topic_name)
|
| 279 |
+
|
| 280 |
+
|
| 281 |
async def _enqueue_resume_followup_with_fallback(
|
| 282 |
*,
|
| 283 |
redis,
|
|
|
|
| 1584 |
session = await redis.hgetall(f"session:{session_id}")
|
| 1585 |
if not session:
|
| 1586 |
return
|
| 1587 |
+
|
| 1588 |
+
max_questions = max(
|
| 1589 |
+
TOPIC_TOTAL_QUESTIONS,
|
| 1590 |
+
_safe_int(session.get("max_questions", TOPIC_TOTAL_QUESTIONS)),
|
| 1591 |
+
)
|
| 1592 |
+
generated_count = _safe_int(session.get("generated_count", 0))
|
| 1593 |
+
remaining_needed = max(0, max_questions - generated_count)
|
| 1594 |
+
|
| 1595 |
+
if remaining_needed <= 0:
|
| 1596 |
+
await redis.hset(f"session:{session_id}", mapping={"topic_followups_generated": "1"})
|
| 1597 |
+
await db[SESSIONS].update_one(
|
| 1598 |
+
{"session_id": session_id},
|
| 1599 |
+
{"$set": {"topic_followups_generated": True, "max_questions": max_questions}},
|
| 1600 |
+
)
|
| 1601 |
+
return
|
| 1602 |
+
|
| 1603 |
if session.get("topic_followups_generated", "0") == "1":
|
| 1604 |
return
|
| 1605 |
|
| 1606 |
qa_pairs = await get_session_qa(session_id)
|
| 1607 |
excluded_questions = await _get_session_question_texts(redis, session_id)
|
| 1608 |
|
| 1609 |
+
ai_target = min(TOPIC_AI_FOLLOWUPS, remaining_needed)
|
| 1610 |
ai_items = await generate_topic_followup_batch(
|
| 1611 |
topic_name=session.get("role_title", "Topic Interview"),
|
| 1612 |
qa_pairs=qa_pairs,
|
| 1613 |
excluded_questions=excluded_questions,
|
| 1614 |
+
count=ai_target,
|
| 1615 |
)
|
| 1616 |
+
db_target = max(0, remaining_needed - len(ai_items))
|
| 1617 |
db_items = await _sample_topic_questions(
|
| 1618 |
db=db,
|
| 1619 |
topic_id=session.get("topic_id", ""),
|
| 1620 |
excluded_questions=excluded_questions + [i.get("question", "") for i in ai_items],
|
| 1621 |
+
limit=db_target,
|
| 1622 |
)
|
| 1623 |
|
| 1624 |
topic_added = 0
|
| 1625 |
+
ai_added = 0
|
| 1626 |
+
db_added = 0
|
| 1627 |
+
for item in ai_items:
|
| 1628 |
qid = await enqueue_question(
|
| 1629 |
redis=redis,
|
| 1630 |
session_id=session_id,
|
|
|
|
| 1636 |
)
|
| 1637 |
if qid:
|
| 1638 |
topic_added += 1
|
| 1639 |
+
ai_added += 1
|
| 1640 |
|
| 1641 |
+
for item in db_items:
|
| 1642 |
+
qid = await enqueue_question(
|
| 1643 |
+
redis=redis,
|
| 1644 |
+
session_id=session_id,
|
| 1645 |
+
question=item.get("question", ""),
|
| 1646 |
+
difficulty=item.get("difficulty", "medium"),
|
| 1647 |
+
category=item.get("category", session.get("role_title", "topic")),
|
| 1648 |
+
ttl_seconds=SESSION_TTL,
|
| 1649 |
+
max_queue_size=MAX_QUEUE_SIZE,
|
| 1650 |
+
)
|
| 1651 |
+
if qid:
|
| 1652 |
+
topic_added += 1
|
| 1653 |
+
db_added += 1
|
| 1654 |
+
|
| 1655 |
+
fallback_added = 0
|
| 1656 |
+
fallback_variants = max(10, remaining_needed * 4)
|
| 1657 |
+
for variant in range(fallback_variants):
|
| 1658 |
+
if topic_added >= remaining_needed:
|
| 1659 |
+
break
|
| 1660 |
+
|
| 1661 |
+
next_question_number = generated_count + topic_added + 1
|
| 1662 |
+
fallback_text = _build_topic_resilient_followup_question(
|
| 1663 |
+
session=session,
|
| 1664 |
+
question_number=next_question_number,
|
| 1665 |
+
variant=variant,
|
| 1666 |
+
)
|
| 1667 |
+
qid = await enqueue_question(
|
| 1668 |
+
redis=redis,
|
| 1669 |
+
session_id=session_id,
|
| 1670 |
+
question=fallback_text,
|
| 1671 |
+
difficulty="medium",
|
| 1672 |
+
category="topic-fallback",
|
| 1673 |
+
ttl_seconds=SESSION_TTL,
|
| 1674 |
+
max_queue_size=MAX_QUEUE_SIZE,
|
| 1675 |
+
)
|
| 1676 |
+
if qid:
|
| 1677 |
+
topic_added += 1
|
| 1678 |
+
fallback_added += 1
|
| 1679 |
+
|
| 1680 |
+
generated_count += topic_added
|
| 1681 |
await _apply_generation_metric_delta(
|
| 1682 |
db=db,
|
| 1683 |
redis=redis,
|
| 1684 |
session_id=session_id,
|
| 1685 |
session=session,
|
| 1686 |
metrics_delta={
|
| 1687 |
+
"gemini_calls": 1 if ai_target > 0 else 0,
|
| 1688 |
+
"gemini_questions": ai_added,
|
| 1689 |
+
"bank_questions": db_added + fallback_added,
|
| 1690 |
+
"bank_shortfall": max(0, remaining_needed - topic_added),
|
| 1691 |
"generation_batches": 1,
|
| 1692 |
},
|
| 1693 |
generated_count=generated_count,
|
| 1694 |
+
extra_redis_fields={
|
| 1695 |
+
"topic_followups_generated": "1",
|
| 1696 |
+
"max_questions": str(max_questions),
|
| 1697 |
+
},
|
| 1698 |
+
extra_db_fields={
|
| 1699 |
+
"topic_followups_generated": True,
|
| 1700 |
+
"max_questions": max_questions,
|
| 1701 |
+
},
|
| 1702 |
)
|
| 1703 |
|
| 1704 |
await flush_backlog_to_queue(
|
|
|
|
| 1798 |
{"$set": {"max_questions": max_questions}},
|
| 1799 |
)
|
| 1800 |
|
| 1801 |
+
if interview_type == "topic" and max_questions < TOPIC_TOTAL_QUESTIONS:
|
| 1802 |
+
max_questions = TOPIC_TOTAL_QUESTIONS
|
| 1803 |
+
await redis.hset(f"session:{session_id}", mapping={"max_questions": str(max_questions)})
|
| 1804 |
+
await db[SESSIONS].update_one(
|
| 1805 |
+
{"session_id": session_id},
|
| 1806 |
+
{"$set": {"max_questions": max_questions}},
|
| 1807 |
+
)
|
| 1808 |
+
|
| 1809 |
_update_local_summary(session_id, current_question_text, answer)
|
| 1810 |
await push_context_item(
|
| 1811 |
redis=redis,
|
|
|
|
| 1916 |
)
|
| 1917 |
next_question_id, q_data = await pop_next_question(redis, session_id)
|
| 1918 |
|
| 1919 |
+
if (
|
| 1920 |
+
not next_question_id
|
| 1921 |
+
and interview_type == "topic"
|
| 1922 |
+
and answered_count < max_questions
|
| 1923 |
+
):
|
| 1924 |
+
# Topic follow-up generation runs in background, so synchronously top-up once
|
| 1925 |
+
# before concluding interview to avoid premature completion around Q4.
|
| 1926 |
+
await _post_submit_topic_processing(
|
| 1927 |
+
session_id=session_id,
|
| 1928 |
+
answered_count=answered_count,
|
| 1929 |
+
)
|
| 1930 |
+
await flush_backlog_to_queue(
|
| 1931 |
+
redis=redis,
|
| 1932 |
+
session_id=session_id,
|
| 1933 |
+
ttl_seconds=SESSION_TTL,
|
| 1934 |
+
max_queue_size=MAX_QUEUE_SIZE,
|
| 1935 |
+
)
|
| 1936 |
+
next_question_id, q_data = await pop_next_question(redis, session_id)
|
| 1937 |
+
|
| 1938 |
if not next_question_id or not q_data:
|
| 1939 |
await redis.hset(
|
| 1940 |
f"session:{session_id}",
|