Add context_quality_score
This commit is contained in:
parent
4cc97efeba
commit
79b773f843
|
@ -27,7 +27,12 @@ def context_rank_node(state: EmailGenerationState) -> EmailGenerationState:
|
||||||
|
|
||||||
ranked_context = retrieval_service.build_context(ranked_chunks)
|
ranked_context = retrieval_service.build_context(ranked_chunks)
|
||||||
|
|
||||||
|
context_quality_score = retrieval_service.calculate_context_quality_score(
|
||||||
|
ranked_chunks
|
||||||
|
)
|
||||||
|
|
||||||
state["ranked_context"] = ranked_context
|
state["ranked_context"] = ranked_context
|
||||||
|
state["context_quality_score"] = context_quality_score
|
||||||
return state
|
return state
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
|
@ -40,6 +40,7 @@ def guardrails_node(state: EmailGenerationState) -> EmailGenerationState:
|
||||||
llm_output = state.get("llm_output")
|
llm_output = state.get("llm_output")
|
||||||
lead_model = state.get("lead_model")
|
lead_model = state.get("lead_model")
|
||||||
ranked_context = state.get("ranked_context")
|
ranked_context = state.get("ranked_context")
|
||||||
|
context_quality_score = state.get("context_quality_score")
|
||||||
|
|
||||||
meta = {
|
meta = {
|
||||||
"locale": lead_model.locale if lead_model else "ru",
|
"locale": lead_model.locale if lead_model else "ru",
|
||||||
|
@ -50,6 +51,9 @@ def guardrails_node(state: EmailGenerationState) -> EmailGenerationState:
|
||||||
"tokens_completion": llm_output.tokens_completion if llm_output else 0,
|
"tokens_completion": llm_output.tokens_completion if llm_output else 0,
|
||||||
"guardrails_violations": len(violations),
|
"guardrails_violations": len(violations),
|
||||||
"context_chunks_used": len(ranked_context.chunks) if ranked_context else 0,
|
"context_chunks_used": len(ranked_context.chunks) if ranked_context else 0,
|
||||||
|
"context_quality_score": (
|
||||||
|
context_quality_score if context_quality_score is not None else 0.0
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
email_clean = EmailDraftClean(
|
email_clean = EmailDraftClean(
|
||||||
|
|
|
@ -20,6 +20,7 @@ class EmailGenerationState(TypedDict):
|
||||||
retrieval_query: Optional[RetrievalQuery]
|
retrieval_query: Optional[RetrievalQuery]
|
||||||
retrieved_chunks: Optional[List[DocChunk]]
|
retrieved_chunks: Optional[List[DocChunk]]
|
||||||
ranked_context: Optional[RankedContext]
|
ranked_context: Optional[RankedContext]
|
||||||
|
context_quality_score: Optional[float]
|
||||||
prompt_payload: Optional[PromptPayload]
|
prompt_payload: Optional[PromptPayload]
|
||||||
llm_output: Optional[LLMRawOutput]
|
llm_output: Optional[LLMRawOutput]
|
||||||
email_draft: Optional[EmailDraft]
|
email_draft: Optional[EmailDraft]
|
||||||
|
|
|
@ -254,7 +254,7 @@ class MarkdownLoader:
|
||||||
r"hello@konsol\.pro",
|
r"hello@konsol\.pro",
|
||||||
r"\+7 \(\d{3}\) \d{3}-\d{2}-\d{2}",
|
r"\+7 \(\d{3}\) \d{3}-\d{2}-\d{2}",
|
||||||
r"125047.*?дом \d+",
|
r"125047.*?дом \d+",
|
||||||
r"\[Разработка — SKDO\]\(\)",
|
r"\[Разработка - SKDO\]\(\)",
|
||||||
r"\[Подключиться к Консоли\]\(\)",
|
r"\[Подключиться к Консоли\]\(\)",
|
||||||
r"\[Кейсы наших клиентов\]\(\)",
|
r"\[Кейсы наших клиентов\]\(\)",
|
||||||
r"\[Делимся экспертизой\]\(\)",
|
r"\[Делимся экспертизой\]\(\)",
|
||||||
|
|
|
@ -3,7 +3,7 @@ from src.models.email import RankedContext
|
||||||
from src.app.config import settings
|
from src.app.config import settings
|
||||||
|
|
||||||
|
|
||||||
SYSTEM_PROMPT = """Вы — AI-ассистент отдела продаж платформы «Консоль.Про». Ваша задача: на основе профиля лида и предоставленного контекста сформировать одно персонализированное холодное письмо (первое касание) на языке лида. Вы пишете кратко, уважительно, без давления. Вы показываете ценность через конкретные результаты клиентов и цифры. Вы не придумываете факты; используете только контекст. Если нет данных или они не релевантны — говорите общими преимуществами платформы.
|
SYSTEM_PROMPT = """Вы - AI-ассистент отдела продаж платформы «Консоль.Про». Ваша задача: на основе профиля лида и предоставленного контекста сформировать одно персонализированное холодное письмо (первое касание) на языке лида. Вы пишете кратко, уважительно, без давления. Вы показываете ценность через конкретные результаты клиентов и цифры. Вы не придумываете факты; используете только контекст. Если нет данных или они не релевантны - говорите общими преимуществами платформы.
|
||||||
Формат ответа строго JSON: {"subject": str, "body": str, "short_reasoning": str, "used_chunks": [ids...]}. Без тройных кавычек, без Markdown."""
|
Формат ответа строго JSON: {"subject": str, "body": str, "short_reasoning": str, "used_chunks": [ids...]}. Без тройных кавычек, без Markdown."""
|
||||||
|
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ USER_PROMPT_TEMPLATE = """[ПРОФИЛЬ ЛИДА]
|
||||||
- Тон: деловой, лаконичный, дружелюбный, с ноткой эмпатии.
|
- Тон: деловой, лаконичный, дружелюбный, с ноткой эмпатии.
|
||||||
- Письмо ≤ 1000 символов, раздели на микро‑абзацы по 1–2 предложения, чтобы текст легко читался.
|
- Письмо ≤ 1000 символов, раздели на микро‑абзацы по 1–2 предложения, чтобы текст легко читался.
|
||||||
- Открывай цепляющим вопросом или заявлением («Как насчёт…?», «Представьте, что…»).
|
- Открывай цепляющим вопросом или заявлением («Как насчёт…?», «Представьте, что…»).
|
||||||
- 1 CTA: предложить 15‑минутный звонок/демо. Не навязчиво: «Если вам интересно —», «Буду рад(а) обсудить» и т.п.
|
- 1 CTA: предложить 15‑минутный звонок/демо. Не навязчиво: «Если вам интересно -», «Буду рад(а) обсудить» и т.п.
|
||||||
- Основная боль ↔ основное решение в первых 2–3 абзацах.
|
- Основная боль ↔ основное решение в первых 2–3 абзацах.
|
||||||
- Добавь конкретные цифры или проценты («до 100 % собираемости документов», «сокращение времени на 80 %»).
|
- Добавь конкретные цифры или проценты («до 100 % собираемости документов», «сокращение времени на 80 %»).
|
||||||
- Вставь мини‑соцдоказательство (название клиента + результат (Конкретизировать кейс цифрой ОБЯЗАТЕЛЬНО)) как отдельный абзац или буллет.
|
- Вставь мини‑соцдоказательство (название клиента + результат (Конкретизировать кейс цифрой ОБЯЗАТЕЛЬНО)) как отдельный абзац или буллет.
|
||||||
|
@ -79,10 +79,10 @@ class PromptBuilder:
|
||||||
hooks = {
|
hooks = {
|
||||||
"marketing_agency": "В маркетинговых агентствах часто сложно быстро подключать десятки подрядчиков для проектов",
|
"marketing_agency": "В маркетинговых агентствах часто сложно быстро подключать десятки подрядчиков для проектов",
|
||||||
"logistics": "В логистических компаниях управление множественными исполнителями требует особого внимания к документообороту",
|
"logistics": "В логистических компаниях управление множественными исполнителями требует особого внимания к документообороту",
|
||||||
"software": "В IT-компаниях работа с фрилансерами и ИП — важная часть команды разработки",
|
"software": "В IT-компаниях работа с фрилансерами и ИП - важная часть команды разработки",
|
||||||
"retail": "В розничной торговле сезонные пики требуют быстрого масштабирования команды исполнителей",
|
"retail": "В розничной торговле сезонные пики требуют быстрого масштабирования команды исполнителей",
|
||||||
"consulting": "В консалтинговых компаниях привлечение экспертов под проекты — критичный процесс",
|
"consulting": "В консалтинговых компаниях привлечение экспертов под проекты - критичный процесс",
|
||||||
"construction": "В строительстве координация подрядчиков и документооборот — ключевые вызовы",
|
"construction": "В строительстве координация подрядчиков и документооборот - ключевые вызовы",
|
||||||
"other": "При работе с внешними исполнителями всегда актуальны вопросы автоматизации процессов",
|
"other": "При работе с внешними исполнителями всегда актуальны вопросы автоматизации процессов",
|
||||||
}
|
}
|
||||||
return hooks.get(industry_tag, hooks["other"])
|
return hooks.get(industry_tag, hooks["other"])
|
||||||
|
|
|
@ -166,3 +166,39 @@ class RetrievalService:
|
||||||
total_tokens=total_tokens,
|
total_tokens=total_tokens,
|
||||||
summary_bullets=summary_bullets,
|
summary_bullets=summary_bullets,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def calculate_context_quality_score(self, chunks: List[DocChunk]) -> float:
|
||||||
|
if not chunks:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
scores = [
|
||||||
|
chunk.similarity_score
|
||||||
|
for chunk in chunks
|
||||||
|
if chunk.similarity_score is not None
|
||||||
|
]
|
||||||
|
|
||||||
|
if not scores:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
mean_similarity = sum(scores) / len(scores)
|
||||||
|
|
||||||
|
high_quality_ratio = sum(1 for score in scores if score > 0.7) / len(scores)
|
||||||
|
|
||||||
|
if len(scores) > 1:
|
||||||
|
variance = sum((score - mean_similarity) ** 2 for score in scores) / len(
|
||||||
|
scores
|
||||||
|
)
|
||||||
|
consistency_score = max(0, 1 - variance * 2)
|
||||||
|
else:
|
||||||
|
consistency_score = 1.0
|
||||||
|
|
||||||
|
quantity_factor = min(1.0, len(chunks) / 3.0)
|
||||||
|
|
||||||
|
quality_score = (
|
||||||
|
0.40 * mean_similarity
|
||||||
|
+ 0.25 * high_quality_ratio
|
||||||
|
+ 0.20 * consistency_score
|
||||||
|
+ 0.15 * quantity_factor
|
||||||
|
)
|
||||||
|
|
||||||
|
return max(0.0, min(1.0, quality_score))
|
||||||
|
|
Loading…
Reference in New Issue