venthrive-api / routes /planning.py
GitHub Actions
Sync from GitHub · 2026-09-26 23:46 UTC
020705e
Raw History Blame Contribute Delete
4.81 kB
"""
Planning AI Suggestions
──────────────────────────────────────────────────────────────────────────────
Reads the business profile and generates actionable task / milestone suggestions
grounded in the BFF data and "Built on Rock" philosophy.
Routes:
POST /api/planning/suggest → task or milestone suggestions
"""
import json
from datetime import datetime, timedelta
from fastapi import APIRouter, HTTPException, Depends
from pydantic import BaseModel
from firebase_admin import firestore
from utils.rate_limiter import with_rate_limit
from utils.groq_client import call_groq_json, HEAVY_MODEL
from utils.pinecone_client import query_book_context
router = APIRouter()
class SuggestRequest(BaseModel):
userId: str
businessId: str
type: str # "tasks" | "milestones"
existing: list[str] = [] # existing task/milestone titles (to avoid duplicates)
def _read_context(user_id: str, business_id: str) -> dict:
db = firestore.client()
biz = (
db.collection("user_data").document(user_id)
.collection("businesses").document(business_id)
)
ctx: dict = {}
for key in ["businessIdentity", "marketData", "businessModel"]:
doc = biz.collection("profile_output").document(key).get()
if doc.exists:
ctx[key] = doc.to_dict()
s1 = biz.collection("bff").document("section1").get()
if s1.exists:
ctx["section1"] = s1.to_dict()
return ctx
@router.post("/suggest")
def suggest_planning_items(
body: SuggestRequest,
user: dict = Depends(with_rate_limit("generation")),
):
if body.type not in ("tasks", "milestones"):
raise HTTPException(status_code=400, detail="type must be 'tasks' or 'milestones'")
ctx = _read_context(body.userId, body.businessId)
biz_name = ctx.get("businessIdentity", {}).get("businessName", "this business")
sector = ctx.get("businessIdentity", {}).get("sector", "")
value_prop = ctx.get("businessIdentity", {}).get("valueProposition", "")
summary = ctx.get("businessIdentity", {}).get("summary", "")
book_ctx = query_book_context(
f"action plan milestones tasks Nigerian entrepreneur {sector}", top_k=2
)
existing_str = (
f"\nDo NOT duplicate any of these existing items: {body.existing}"
if body.existing else ""
)
system = (
"You are a business consultant helping a Nigerian entrepreneur plan their next steps. "
"Suggest practical, specific, achievable actions grounded in the Business Footing Framework. "
"All suggestions must be relevant to their specific business — no generic advice. "
"Write in plain English. Return ONLY valid JSON."
+ (f"\n\n{book_ctx}" if book_ctx else "")
)
if body.type == "tasks":
prompt = f"""
Business: {biz_name} ({sector})
Value proposition: {value_prop}
Summary: {summary[:400]}
{existing_str}
Generate exactly 5 actionable Kanban tasks for this business right now.
Tasks should be specific, achievable within 2 weeks, and move the business forward.
Spread them across different priorities: some for this week, some for next week.
Return JSON:
{{
"suggestions": [
{{
"title": "specific task title",
"description": "why this matters for {biz_name}",
"column": "todo"
}},
...
]
}}
column must be one of: "todo", "in_progress", "done"
Most tasks should be "todo" unless clearly already started.
"""
else: # milestones
today = datetime.now()
dates = [(today + timedelta(days=d)).strftime("%Y-%m-%d") for d in [30, 60, 90, 180, 365]]
prompt = f"""
Business: {biz_name} ({sector})
Value proposition: {value_prop}
Summary: {summary[:400]}
{existing_str}
Generate exactly 5 business milestones for the next 12 months.
Milestones should be measurable achievements that mark significant progress.
Space them across different timeframes.
Available target dates to choose from: {dates}
(Choose the most appropriate date for each milestone)
Return JSON:
{{
"suggestions": [
{{
"title": "milestone title (specific and measurable)",
"targetDate": "YYYY-MM-DD"
}},
...
]
}}
"""
try:
result = call_groq_json(
system, prompt, model=HEAVY_MODEL, max_tokens=800,
fixture=f"planning_{body.type}",
)
suggestions = result.get("suggestions", [])
if not suggestions:
raise ValueError("No suggestions returned")
return {"suggestions": suggestions}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Suggestion generation failed: {str(e)}")