from __future__ import annotations from ..models import ExpertReport from subenvs.email.hr_tools import build_hr_memo, score_memo from subenvs.email.graders import grade_response # Maps CoS brief tasks to the Round 1 email-env task IDs so the right # task-specific rubric fires inside grade_response. _BRIEF_TO_EMAIL_TASK = { "easy_brief": "task_1", "medium_brief": "task_2", "hard_brief": "task_3", "expert_brief": "task_3", } _HR_QUERY = ( "memo style guide audience tone bullet highlights " "call to action professional direct exemplar" ) def _clamp(score: float) -> float: """Match ceo_brief_env.graders._clamp so every emitted score is in (0.001, 0.999).""" return max(0.001, min(0.999, round(float(score), 4))) class HRExpert: expert_id = "hr" def run( self, task_name: str, task_meta: dict, analyst_report: ExpertReport | None, finance_report: ExpertReport | None, strategy_report: ExpertReport | None = None, focused: bool = False, use_rag: bool = False, ) -> ExpertReport: # --- 1. Build the memo from upstream reports ----------------------- highlights: list[str] = [] if analyst_report: highlights.extend(analyst_report.bullet_points[:2]) if finance_report: highlights.extend(finance_report.bullet_points[:2]) if strategy_report: highlights.extend(strategy_report.bullet_points[:2]) memory_citations: list[str] = [] memory_snippets: list[str] = [] if use_rag: from memory import get_retriever hits = get_retriever().query(_HR_QUERY, k=2) memory_citations = [h.as_citation() for h in hits] memory_snippets = [h.snippet for h in hits] if hits: highlights.insert( 0, f"Memo follows {hits[0].source.split('#')[0]} (style SOP retrieved from company memory).", ) audience = str(task_meta.get("memo_audience", "team")) title = str(task_meta.get("title", task_name)) memo = build_hr_memo(audience, title, highlights) # --- 2. Three-part scoring ---------------------------------------- structure_score = score_memo(memo, task_meta.get("hr_required_terms", [])) email_task_id = _BRIEF_TO_EMAIL_TASK.get(task_name, "task_1") tone_score = grade_response( email=task_meta.get("instruction", ""), response=memo, task_id=email_task_id, ) audience_hit = 1.0 if audience.lower() in memo.lower() else 0.0 blended = 0.45 * structure_score + 0.45 * tone_score + 0.10 * audience_hit score = _clamp(blended) # --- 3. Return a typed ExpertReport ------------------------------- issues: list[str] = [] if structure_score < 0.4: issues.append("hr:weak_structure_or_missing_required_terms") if tone_score < 0.4: issues.append("hr:weak_professional_tone") if audience_hit == 0.0: issues.append(f"hr:audience_not_referenced:{audience}") return ExpertReport( expert_id="hr", title="HR / Communications Memo", summary=( "Drafted the internal memo and scored it on structure, " "professional tone, and audience relevance." ), metrics={ "memo_structure_score": _clamp(structure_score), "memo_tone_score": _clamp(tone_score), "audience_reference": audience_hit, "memo_score": score, }, bullet_points=[ f"Memo addressed to {audience}.", f"Structure score {structure_score:.3f}, tone score {tone_score:.3f}.", "Blended score uses 45% structure, 45% tone, 10% audience bonus.", ], issues=issues, memo=memo, score=score, memory_citations=memory_citations, memory_snippets=memory_snippets, )