mirror of
https://github.com/x1xhlol/system-prompts-and-models-of-ai-tools.git
synced 2026-06-17 23:09:35 +00:00
fix: Finalize proposals API, sales agent, and quote engine
https://claude.ai/code/session_01LsnvBa7HwF5hs99VZbgLGj
This commit is contained in:
parent
680b82b1e4
commit
2996827f5b
@ -58,16 +58,32 @@ class AcceptRequest(BaseModel):
|
||||
notes: str = ""
|
||||
|
||||
|
||||
class AIProposalRequest(BaseModel):
|
||||
deal_title: str
|
||||
client_name: str
|
||||
client_company: str = ""
|
||||
industry: str = "services"
|
||||
deal_value: float = 0.0
|
||||
currency: str = "SAR"
|
||||
requirements: str = ""
|
||||
language: str = "ar"
|
||||
extra_context: str = ""
|
||||
# ── Helpers ──────────────────────────────────────
|
||||
|
||||
async def _fetch_proposal(db: AsyncSession, proposal_id: UUID, tenant_id) -> Proposal:
|
||||
result = await db.execute(
|
||||
select(Proposal).where(Proposal.id == proposal_id, Proposal.tenant_id == tenant_id)
|
||||
)
|
||||
p = result.scalar_one_or_none()
|
||||
if not p:
|
||||
raise HTTPException(status_code=404, detail="عرض السعر غير موجود")
|
||||
return p
|
||||
|
||||
|
||||
def _proposal_dict(p: Proposal) -> dict:
|
||||
return {
|
||||
"id": str(p.id), "tenant_id": str(p.tenant_id),
|
||||
"deal_id": str(p.deal_id) if p.deal_id else None,
|
||||
"lead_id": str(p.lead_id) if p.lead_id else None,
|
||||
"title": p.title, "content": p.content,
|
||||
"total_amount": str(p.total_amount) if p.total_amount else "0",
|
||||
"currency": p.currency, "status": p.status,
|
||||
"valid_until": p.valid_until.isoformat() if p.valid_until else None,
|
||||
"sent_at": p.sent_at.isoformat() if p.sent_at else None,
|
||||
"viewed_at": p.viewed_at.isoformat() if p.viewed_at else None,
|
||||
"created_at": p.created_at.isoformat() if p.created_at else None,
|
||||
"updated_at": p.updated_at.isoformat() if p.updated_at else None,
|
||||
}
|
||||
|
||||
|
||||
# ── Endpoints ────────────────────────────────────
|
||||
@ -114,19 +130,7 @@ async def create_proposal(
|
||||
):
|
||||
"""Create a new proposal/quote."""
|
||||
engine = QuoteEngine(db)
|
||||
quote_data = QuoteCreate(
|
||||
tenant_id=str(current_user.tenant_id),
|
||||
deal_id=data.deal_id,
|
||||
lead_id=data.lead_id,
|
||||
title=data.title,
|
||||
currency=data.currency,
|
||||
industry=data.industry,
|
||||
validity_days=data.validity_days,
|
||||
vat_registration_number=data.vat_registration_number,
|
||||
client_name=data.client_name,
|
||||
client_company=data.client_company,
|
||||
notes_ar=data.notes_ar,
|
||||
)
|
||||
quote_data = QuoteCreate(tenant_id=str(current_user.tenant_id), **data.model_dump())
|
||||
return await engine.create_quote(quote_data)
|
||||
|
||||
|
||||
@ -176,15 +180,7 @@ async def get_proposal(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Get full proposal details."""
|
||||
result = await db.execute(
|
||||
select(Proposal).where(
|
||||
Proposal.id == proposal_id,
|
||||
Proposal.tenant_id == current_user.tenant_id,
|
||||
)
|
||||
)
|
||||
proposal = result.scalar_one_or_none()
|
||||
if not proposal:
|
||||
raise HTTPException(status_code=404, detail="عرض السعر غير موجود")
|
||||
proposal = await _fetch_proposal(db, proposal_id, current_user.tenant_id)
|
||||
return _proposal_dict(proposal)
|
||||
|
||||
|
||||
@ -196,15 +192,7 @@ async def update_proposal(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Update proposal metadata."""
|
||||
result = await db.execute(
|
||||
select(Proposal).where(
|
||||
Proposal.id == proposal_id,
|
||||
Proposal.tenant_id == current_user.tenant_id,
|
||||
)
|
||||
)
|
||||
proposal = result.scalar_one_or_none()
|
||||
if not proposal:
|
||||
raise HTTPException(status_code=404, detail="عرض السعر غير موجود")
|
||||
proposal = await _fetch_proposal(db, proposal_id, current_user.tenant_id)
|
||||
|
||||
if data.title is not None:
|
||||
proposal.title = data.title
|
||||
@ -268,15 +256,7 @@ async def accept_proposal(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Client acceptance endpoint."""
|
||||
result = await db.execute(
|
||||
select(Proposal).where(
|
||||
Proposal.id == proposal_id,
|
||||
Proposal.tenant_id == current_user.tenant_id,
|
||||
)
|
||||
)
|
||||
proposal = result.scalar_one_or_none()
|
||||
if not proposal:
|
||||
raise HTTPException(status_code=404, detail="عرض السعر غير موجود")
|
||||
proposal = await _fetch_proposal(db, proposal_id, current_user.tenant_id)
|
||||
if proposal.status == QuoteStatus.EXPIRED.value:
|
||||
raise HTTPException(status_code=400, detail="عرض السعر منتهي الصلاحية")
|
||||
|
||||
@ -300,16 +280,7 @@ async def generate_pdf_data(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Generate PDF-ready data for a proposal."""
|
||||
result = await db.execute(
|
||||
select(Proposal).where(
|
||||
Proposal.id == proposal_id,
|
||||
Proposal.tenant_id == current_user.tenant_id,
|
||||
)
|
||||
)
|
||||
proposal = result.scalar_one_or_none()
|
||||
if not proposal:
|
||||
raise HTTPException(status_code=404, detail="عرض السعر غير موجود")
|
||||
|
||||
proposal = await _fetch_proposal(db, proposal_id, current_user.tenant_id)
|
||||
generator = ProposalGenerator()
|
||||
ai_req = ProposalInput(
|
||||
deal_title=proposal.title,
|
||||
@ -323,24 +294,3 @@ async def generate_pdf_data(
|
||||
)
|
||||
ai_proposal = await generator.generate_proposal(ai_req)
|
||||
return await generator.export_pdf_data(ai_proposal)
|
||||
|
||||
|
||||
# ── Helpers ──────────────────────────────────────
|
||||
|
||||
def _proposal_dict(p: Proposal) -> dict:
|
||||
return {
|
||||
"id": str(p.id),
|
||||
"tenant_id": str(p.tenant_id),
|
||||
"deal_id": str(p.deal_id) if p.deal_id else None,
|
||||
"lead_id": str(p.lead_id) if p.lead_id else None,
|
||||
"title": p.title,
|
||||
"content": p.content,
|
||||
"total_amount": str(p.total_amount) if p.total_amount else "0",
|
||||
"currency": p.currency,
|
||||
"status": p.status,
|
||||
"valid_until": p.valid_until.isoformat() if p.valid_until else None,
|
||||
"sent_at": p.sent_at.isoformat() if p.sent_at else None,
|
||||
"viewed_at": p.viewed_at.isoformat() if p.viewed_at else None,
|
||||
"created_at": p.created_at.isoformat() if p.created_at else None,
|
||||
"updated_at": p.updated_at.isoformat() if p.updated_at else None,
|
||||
}
|
||||
|
||||
@ -34,50 +34,23 @@ STATE_TRANSITIONS: dict[str, list[str]] = {
|
||||
}
|
||||
|
||||
INDUSTRY_QUALIFIERS: dict[str, list[str]] = {
|
||||
"real_estate": [
|
||||
"ما نوع العقار المطلوب (سكني/تجاري)؟",
|
||||
"ما المنطقة أو الحي المفضل؟",
|
||||
"ما الميزانية التقريبية؟",
|
||||
"هل تبحث عن شراء أو إيجار؟",
|
||||
],
|
||||
"healthcare": [
|
||||
"ما نوع الخدمة الطبية المطلوبة؟",
|
||||
"هل لديك تأمين طبي؟",
|
||||
"هل تفضل موعد صباحي أو مسائي؟",
|
||||
],
|
||||
"services": [
|
||||
"ما طبيعة الخدمة المطلوبة؟",
|
||||
"ما الميزانية التقريبية؟",
|
||||
"ما الجدول الزمني المتوقع؟",
|
||||
"هل سبق تجربة مزود خدمة آخر؟",
|
||||
],
|
||||
"contracting": [
|
||||
"ما نوع المشروع (بناء/صيانة/تشطيبات)؟",
|
||||
"ما المساحة التقريبية؟",
|
||||
"ما الميزانية المخصصة؟",
|
||||
"هل الموقع في الرياض أو منطقة أخرى؟",
|
||||
],
|
||||
"education": [
|
||||
"ما البرنامج أو الدورة المطلوبة؟",
|
||||
"هل تفضل حضوري أو عن بعد؟",
|
||||
"ما مستوى الخبرة الحالي؟",
|
||||
],
|
||||
"retail": [
|
||||
"ما المنتج أو الفئة المطلوبة؟",
|
||||
"هل تبحث عن كميات تجارية أو شخصية؟",
|
||||
"ما المنطقة لغرض التوصيل؟",
|
||||
],
|
||||
"real_estate": ["ما نوع العقار المطلوب (سكني/تجاري)؟", "ما المنطقة أو الحي المفضل؟",
|
||||
"ما الميزانية التقريبية؟", "هل تبحث عن شراء أو إيجار؟"],
|
||||
"healthcare": ["ما نوع الخدمة الطبية المطلوبة؟", "هل لديك تأمين طبي؟",
|
||||
"هل تفضل موعد صباحي أو مسائي؟"],
|
||||
"services": ["ما طبيعة الخدمة المطلوبة؟", "ما الميزانية التقريبية؟",
|
||||
"ما الجدول الزمني المتوقع؟", "هل سبق تجربة مزود خدمة آخر؟"],
|
||||
"contracting": ["ما نوع المشروع (بناء/صيانة/تشطيبات)؟", "ما المساحة التقريبية؟",
|
||||
"ما الميزانية المخصصة؟", "هل الموقع في الرياض أو منطقة أخرى؟"],
|
||||
"education": ["ما البرنامج أو الدورة المطلوبة؟", "هل تفضل حضوري أو عن بعد؟",
|
||||
"ما مستوى الخبرة الحالي؟"],
|
||||
"retail": ["ما المنتج أو الفئة المطلوبة؟", "هل تبحث عن كميات تجارية أو شخصية؟",
|
||||
"ما المنطقة لغرض التوصيل؟"],
|
||||
}
|
||||
|
||||
ESCALATION_TRIGGERS = [
|
||||
"أبي أكلم مدير",
|
||||
"أبي أتكلم مع شخص",
|
||||
"أبي موظف",
|
||||
"ما فهمت",
|
||||
"مشكلة كبيرة",
|
||||
"شكوى",
|
||||
"غاضب",
|
||||
"مستعجل جداً",
|
||||
"أبي أكلم مدير", "أبي أتكلم مع شخص", "أبي موظف", "ما فهمت",
|
||||
"مشكلة كبيرة", "شكوى", "غاضب", "مستعجل جداً",
|
||||
]
|
||||
|
||||
|
||||
@ -224,7 +197,6 @@ class SalesAgent:
|
||||
return resp.content.strip()
|
||||
|
||||
# ── Internal helpers ─────────────────────────────
|
||||
|
||||
def _get_or_create_context(
|
||||
self, phone: str, lead_id: str, name: str, industry: str,
|
||||
) -> AgentContext:
|
||||
@ -245,31 +217,13 @@ class SalesAgent:
|
||||
current_q = qualifiers[ctx.questions_asked] if ctx.questions_asked < len(qualifiers) else ""
|
||||
|
||||
state_instructions = {
|
||||
ConversationState.GREETING.value: (
|
||||
"رحّب بالعميل بأسلوب سعودي ودود. اسأل كيف تقدر تساعده. "
|
||||
"لا تكن رسمياً أكثر من اللازم."
|
||||
),
|
||||
ConversationState.QUALIFICATION.value: (
|
||||
f"اسأل السؤال التالي بأسلوب طبيعي: {current_q}\n"
|
||||
"حاول استخلاص المعلومات من إجابة العميل."
|
||||
),
|
||||
ConversationState.NEEDS_ANALYSIS.value: (
|
||||
"حلل احتياجات العميل وأكّد فهمك. لخّص ما فهمته واسأل إذا في شي ثاني."
|
||||
),
|
||||
ConversationState.SOLUTION_PITCH.value: (
|
||||
"اعرض الحل المناسب بناءً على احتياجات العميل. "
|
||||
"ركّز على الفوائد مع ذكر القيمة بشكل غير مباشر."
|
||||
),
|
||||
ConversationState.OBJECTION_HANDLING.value: (
|
||||
"تعامل مع اعتراض العميل بذكاء. أعد التأطير وركّز على القيمة. "
|
||||
"لا تكن دفاعياً."
|
||||
),
|
||||
"greeting": "رحّب بالعميل بأسلوب سعودي ودود. اسأل كيف تقدر تساعده.",
|
||||
"qualification": f"اسأل السؤال التالي بأسلوب طبيعي: {current_q}\nحاول استخلاص المعلومات من إجابة العميل.",
|
||||
"needs_analysis": "حلل احتياجات العميل وأكّد فهمك. لخّص ما فهمته واسأل إذا في شي ثاني.",
|
||||
"solution_pitch": "اعرض الحل المناسب بناءً على احتياجات العميل. ركّز على الفوائد.",
|
||||
"objection_handling": "تعامل مع اعتراض العميل بذكاء. أعد التأطير وركّز على القيمة.",
|
||||
}
|
||||
|
||||
instruction = state_instructions.get(
|
||||
ctx.state,
|
||||
"أكمل المحادثة بأسلوب مهني وودود.",
|
||||
)
|
||||
instruction = state_instructions.get(ctx.state, "أكمل المحادثة بأسلوب مهني وودود.")
|
||||
|
||||
system = (
|
||||
"أنت وكيل مبيعات ذكي لشركة سعودية. تتحدث بالعامية السعودية الراقية.\n"
|
||||
|
||||
@ -135,16 +135,11 @@ class QuoteEngine:
|
||||
|
||||
content: dict = dict(proposal.content)
|
||||
line_items: list = list(content.get("line_items", []))
|
||||
|
||||
line_total = item.unit_price * item.quantity
|
||||
line_items.append({
|
||||
"id": str(uuid.uuid4())[:8],
|
||||
"description_ar": item.description_ar,
|
||||
"description_en": item.description_en,
|
||||
"quantity": item.quantity,
|
||||
"unit_price": str(item.unit_price),
|
||||
"unit": item.unit,
|
||||
"total": str(line_total),
|
||||
"id": str(uuid.uuid4())[:8], "description_ar": item.description_ar,
|
||||
"description_en": item.description_en, "quantity": item.quantity,
|
||||
"unit_price": str(item.unit_price), "unit": item.unit, "total": str(line_total),
|
||||
})
|
||||
content["line_items"] = line_items
|
||||
proposal.content = content
|
||||
|
||||
Loading…
Reference in New Issue
Block a user