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