| """Deal phase start helpers (Phase 9B).""" |
|
|
| from __future__ import annotations |
|
|
| import logging |
| import uuid |
| from datetime import datetime, timezone |
| from typing import Any |
|
|
| from core.deal_persona_builder import DEAL_TYPE_LABELS, build_deal_system_prompt, get_persona_display |
| from core.judge_settings import get_label |
| from core import model_router |
|
|
| logger = logging.getLogger(__name__) |
|
|
| NEGOTIATION_TAGS = [ |
| "Anchoring", |
| "Evidence Quality", |
| "Concession Control", |
| "Alternatives", |
| "Value Articulation", |
| "Closing Move", |
| ] |
|
|
| _DEAL_FIRST_FALLBACK: dict[str, str] = { |
| "equity": ( |
| "You mentioned traction in your pitch, but it is not enough for your full ask. " |
| "I would open below your terms. Convince me why I should move closer to what you want." |
| ), |
| "mentorship": ( |
| "You clearly need help sharpening execution. I can offer limited mentorship time, " |
| "but I need you to define exactly what outcome you want from me." |
| ), |
| "pilot": ( |
| "I am open to a pilot, but I will not approve a long free trial without risk controls. " |
| "What paid pilot structure can you defend?" |
| ), |
| "sponsorship": ( |
| "I can consider sponsorship, but I need measurable return and audience quality. " |
| "What do we get for the budget you are asking?" |
| ), |
| } |
|
|
|
|
| def get_deal_negotiation_tags(deal_type: str) -> list[str]: |
| return list(NEGOTIATION_TAGS) |
|
|
|
|
| def build_deal_context(session: dict) -> dict[str, Any]: |
| """Build deal context from session, scorecard, and verdict.""" |
| startup = session.get("startup", {}) or {} |
| verdict = session.get("judge_verdict") or {} |
| scorecard = session.get("latest_scorecard") or {} |
| difficulty_profile = session.get("difficulty_profile") or session.get("difficulty", "practice") |
| deal_type = session.get("deal_type") or verdict.get("deal_type", "equity") |
|
|
| return { |
| "ask": str(startup.get("ask", "")).strip(), |
| "opening_offer": str(verdict.get("deal_opening_offer", "")).strip(), |
| "founder_position": str(startup.get("ask", "")).strip(), |
| "judge_position": str(verdict.get("deal_opening_offer", "")).strip(), |
| "deal_type": deal_type, |
| "deal_type_label": DEAL_TYPE_LABELS.get(deal_type, deal_type), |
| "pitch_overall": scorecard.get("overall", 0), |
| "traction": str(startup.get("traction", "")).strip(), |
| "startup_name": str(startup.get("name", "")).strip(), |
| "difficulty_profile": difficulty_profile, |
| "difficulty_label": get_label(difficulty_profile), |
| } |
|
|
|
|
| def _build_first_deal_messages(session: dict, deal_context: dict) -> list[dict[str, str]]: |
| system = build_deal_system_prompt(session, deal_context, "Anchoring") |
| startup = session.get("startup", {}) or {} |
| verdict = session.get("judge_verdict") or {} |
|
|
| user = ( |
| f"Transition from pitch to deal negotiation.\n" |
| f"Startup: {startup.get('name', '')}\n" |
| f"Traction: {startup.get('traction', '')}\n" |
| f"Founder's ask: {deal_context.get('ask', '')}\n" |
| f"Your opening offer: {deal_context.get('opening_offer', '')}\n" |
| f"Verdict reaction: {verdict.get('judge_reaction', '')}\n\n" |
| "Deliver your first deal negotiation message. Reference the pitch if possible. " |
| "1-3 sentences. One pushback or opening terms. Plain text only." |
| ) |
| return [{"role": "system", "content": system}, {"role": "user", "content": user}] |
|
|
|
|
| def build_first_deal_message(session: dict, deal_context: dict) -> dict[str, str]: |
| """Generate first deal message via Nemotron with local fallback.""" |
| deal_type = deal_context.get("deal_type", "equity") |
| fallback = _DEAL_FIRST_FALLBACK.get(deal_type, _DEAL_FIRST_FALLBACK["equity"]) |
|
|
| verdict = session.get("judge_verdict") or {} |
| if verdict.get("judge_reaction"): |
| fallback = str(verdict["judge_reaction"]) |
| if deal_context.get("opening_offer"): |
| fallback += f" {deal_context['opening_offer']}" |
|
|
| messages = _build_first_deal_messages(session, deal_context) |
| model_mode = session.get("model_mode", "premium_nvidia") |
| result = model_router.generate_deal_round_response(messages, model_mode=model_mode) |
|
|
| if result.get("ok") and result.get("content"): |
| text = str(result["content"]).strip() |
| if text: |
| return {"ai_message": text[:600], "provider": result.get("provider", "nvidia")} |
|
|
| logger.warning("deal_phase: first message fallback — %s", result.get("error")) |
| return {"ai_message": fallback[:600], "provider": "local"} |
|
|
|
|
| def start_deal_phase(session: dict) -> dict[str, Any]: |
| """Initialize deal phase on existing pitch session.""" |
| if not session.get("latest_scorecard"): |
| return {"error": "No scorecard found. End a battle before starting deal phase."} |
|
|
| verdict = session.get("judge_verdict") |
| if not verdict: |
| return {"error": "No judge verdict found. End a battle first."} |
|
|
| if not verdict.get("can_continue_to_deal"): |
| return { |
| "error": ( |
| f"Deal phase cannot start because the judge verdict is " |
| f"{verdict.get('interest_level', 'no_interest')}." |
| ) |
| } |
|
|
| if session.get("deal_phase_active") and session.get("deal_history"): |
| deal_context = session.get("deal_context") or build_deal_context(session) |
| last_judge = next( |
| (h for h in reversed(session.get("deal_history", [])) if h.get("role") == "judge"), |
| None, |
| ) |
| return { |
| "session_id": session.get("session_id", ""), |
| "deal_phase_id": session.get("deal_phase_id", ""), |
| "deal_type": session.get("deal_type", ""), |
| "persona_name": verdict.get("persona_name", ""), |
| "persona_role": verdict.get("persona_type", ""), |
| "round": session.get("deal_round", 1), |
| "negotiation_tag": last_judge.get("negotiation_tag", "Anchoring") if last_judge else "Anchoring", |
| "ai_message": last_judge.get("message", "") if last_judge else "", |
| "deal_context": deal_context, |
| "can_continue": True, |
| "soft_limit_reached": False, |
| } |
|
|
| deal_phase_id = str(uuid.uuid4()) |
| deal_type = verdict.get("deal_type", "equity") |
| deal_context = build_deal_context(session) |
| persona_name, persona_role = get_persona_display(session.get("persona", "skeptical_vc")) |
|
|
| first = build_first_deal_message(session, deal_context) |
| ai_message = first["ai_message"] |
| tag = "Anchoring" |
|
|
| session["deal_phase_active"] = True |
| session["deal_phase_id"] = deal_phase_id |
| session["deal_type"] = deal_type |
| session["deal_context"] = deal_context |
| session["deal_round"] = 1 |
| session["deal_history"] = [{ |
| "round": 1, |
| "role": "judge", |
| "message": ai_message, |
| "negotiation_tag": tag, |
| "answer_quality": "", |
| "action": "opening", |
| "input_mode": "", |
| "voice_turn_id": "", |
| "timestamp": datetime.now(timezone.utc).isoformat(), |
| }] |
| session["deal_scorecard"] = {} |
| session["combined_scorecard"] = {} |
|
|
| return { |
| "session_id": session.get("session_id", ""), |
| "deal_phase_id": deal_phase_id, |
| "deal_type": deal_type, |
| "persona_name": persona_name, |
| "persona_role": persona_role, |
| "round": 1, |
| "negotiation_tag": tag, |
| "ai_message": ai_message, |
| "deal_context": deal_context, |
| "can_continue": True, |
| "soft_limit_reached": False, |
| } |
|
|