File size: 6,217 Bytes
e561008
 
 
 
 
889e86b
e561008
 
89a70e2
e561008
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
da1822e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e561008
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89a70e2
e561008
 
89a70e2
 
 
e561008
 
 
 
 
 
 
 
 
 
 
 
 
 
 
889e86b
e561008
 
 
94dd7b2
 
e561008
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
da1822e
e561008
 
 
 
 
 
 
 
 
020705e
da1822e
e561008
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
import json
from fastapi import APIRouter, HTTPException, Depends
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
from firebase_admin import firestore
from utils.rate_limiter import with_rate_limit
from utils.groq_client import call_groq_stream, LIGHT_MODEL, HEAVY_MODEL
from utils.pinecone_client import query_book_context
from utils import business_type

router = APIRouter()


# ─── Schemas ──────────────────────────────────────────────────────────────────

class Message(BaseModel):
    role: str    # "user" or "assistant"
    content: str


class ChatRequest(BaseModel):
    businessId: str
    sessionId: str
    message: str
    history: list[Message] = []
    useHeavyModel: bool = False   # True for AI Consultation sessions


# ─── Helpers ──────────────────────────────────────────────────────────────────

HISTORY_LIMIT = 10          # last N messages kept for context window efficiency
HISTORY_CONTENT_CHARS = 4000  # per-message cap so one huge paste can't bloat every call


def build_messages(system_prompt: str, history: list[Message], message: str) -> list[dict]:
    """
    System prompt + recent history + the new message. Only 'user' and
    'assistant' roles are accepted from the client: history comes from the
    browser, so a 'system' role in it would let a caller inject instructions.
    """
    messages = [{"role": "system", "content": system_prompt}]
    for msg in history[-HISTORY_LIMIT:]:
        if msg.role in ("user", "assistant"):
            messages.append({"role": msg.role, "content": msg.content[:HISTORY_CONTENT_CHARS]})
    messages.append({"role": "user", "content": message})
    return messages

def _get_business_context(user_id: str, business_id: str) -> str:
    """Builds a system context string from BFF + profile_output in Firestore."""
    db = firestore.client()
    biz_ref = (
        db.collection("user_data")
        .document(user_id)
        .collection("businesses")
        .document(business_id)
    )

    # Fetch BFF section 1 (WHAT) for quick context
    s1 = biz_ref.collection("bff").document("section1").get()
    # Fetch business identity if generated
    identity = biz_ref.collection("profile_output").document("businessIdentity").get()

    ctx_parts = []

    if s1.exists:
        d = s1.to_dict()
        t = business_type.describe(d)
        ctx_parts.append(
            f"Business: {d.get('businessName', 'Unknown')}, "
            f"Sector: {t['sector']}, "
            f"Type of business: {t['subsector']}, "
            f"Niche: {t['niche']}"
        )

    if identity.exists:
        d = identity.to_dict()
        ctx_parts.append(f"Summary: {d.get('summary', '')}")
        ctx_parts.append(f"Value Proposition: {d.get('valueProposition', '')}")

    return "\n".join(ctx_parts) if ctx_parts else "Business profile not yet generated."


# ─── Route: Chat message (streaming SSE) ─────────────────────────────────────

@router.post("/message")
def chat_message(
    body: ChatRequest,
    user: dict = Depends(with_rate_limit("chat")),
):
    """
    Accepts a user message and returns a streaming SSE response.
    For passive AI Chat: uses light model (LIGHT_MODEL).
    For AI Consultation: uses heavy model (HEAVY_MODEL).

    Stream format (SSE):
      data: {"type": "thinking", "token": "..."}   ← reasoning (heavy model only)
      data: {"type": "response", "token": "..."}   ← main response
      data: {"type": "done"}                        ← stream end
    """
    user_id = user.get("uid")
    model   = HEAVY_MODEL if body.useHeavyModel else LIGHT_MODEL

    business_context = _get_business_context(user_id, body.businessId)

    # Retrieve relevant passages from "Built on Rock" based on user's message
    book_ctx = query_book_context(body.message, top_k=2)

    system_prompt = f"""You are the VenThrive Business Assistant β€” a business consultant trained in the Business Footing Framework (BFF) from "Built on Rock" by Iyanuoluwa Joseph Akinnusi. You help Nigerian entrepreneurs build successful businesses using the WHAT-WHY-HOW philosophy.

BUSINESS CONTEXT:
{business_context}
{f"{chr(10)}RELEVANT BFF GUIDANCE FROM 'BUILT ON ROCK':{chr(10)}{book_ctx}" if book_ctx else ""}

RULES:
1. STRICTLY BUSINESS: Stay focused on the entrepreneur's business. If asked personal questions, redirect warmly.
2. NIGERIAN CONTEXT: Reference real Nigerian examples β€” banks (GTBank, First Bank, Moniepoint), platforms (Jumia, Konga, Flutterwave), markets (Balogun, Alaba), cities and states.
3. BFF PHILOSOPHY: When relevant, ground advice in the WHAT-WHY-HOW framework from "Built on Rock".
4. PLAIN ENGLISH: No business jargon without explanation. Write like you're talking to a smart friend.
5. CONCISE: 5-6 sentences max per response. Use bullet points for lists.
5. ACTIONABLE: Every response should tell the entrepreneur what to DO next, not just what things mean.
"""

    messages = build_messages(system_prompt, body.history, body.message)

    def stream():
        try:
            # call_groq_stream handles key rotation on 429
            stream_response = call_groq_stream(
                system_prompt=system_prompt,
                user_prompt=body.message,
                model=model,
                max_tokens=512,
                fixture="chat_response",
                messages=messages,
            )
            for chunk in stream_response:
                token = chunk.choices[0].delta.content
                if token:
                    yield f"data: {json.dumps({'type': 'response', 'token': token})}\n\n"

            yield f"data: {json.dumps({'type': 'done'})}\n\n"

        except Exception as e:
            yield f"data: {json.dumps({'type': 'error', 'message': 'Something went wrong. Please try again.'})}\n\n"

    return StreamingResponse(stream(), media_type="text/event-stream")