Create app.py
Browse files
app.py
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
|
| 3 |
+
# ------------------------------
|
| 4 |
+
# Data (minimal, in-file)
|
| 5 |
+
# ------------------------------
|
| 6 |
+
RUBRIC_WEIGHTS = {
|
| 7 |
+
"grit": 0.18, "risk": 0.12, "time_per_week": 0.14, "runway_months": 0.16,
|
| 8 |
+
"customer_access": 0.12, "validation_skill": 0.10, "ops_skill": 0.08, "finance_skill": 0.10
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
PATHS = {
|
| 12 |
+
"tech": [
|
| 13 |
+
{"path":"Micro-SaaS","difficulty":3,"benefits":"High margins, scalable, global reach",
|
| 14 |
+
"skills":["customer interviews","MVP scoping","pricing","analytics"],
|
| 15 |
+
"starter":["Define niche pain","5–10 interviews","Landing page test","Iterate"]},
|
| 16 |
+
{"path":"No-code automation agency","difficulty":2,"benefits":"Service revenue funds experiments",
|
| 17 |
+
"skills":["workflow mapping","Zapier/Make","sales","onboarding"],
|
| 18 |
+
"starter":["Pick niche","3 pilot clients","SOPs","Retainers"]},
|
| 19 |
+
{"path":"Educational apps","difficulty":3,"benefits":"Recurring revenue via schools/parents",
|
| 20 |
+
"skills":["curriculum","UX","distribution partnerships"],
|
| 21 |
+
"starter":["Niche curriculum","Prototype","Parent/teacher tests"]}
|
| 22 |
+
],
|
| 23 |
+
"cooking": [
|
| 24 |
+
{"path":"Meal-prep delivery","difficulty":4,"benefits":"Tangible product; recurring subs",
|
| 25 |
+
"skills":["food safety","menu costing","logistics","support"],
|
| 26 |
+
"starter":["Permits","Menu tests","Delivery loop","Subscriptions"]},
|
| 27 |
+
{"path":"Cooking classes","difficulty":2,"benefits":"Low capex; community",
|
| 28 |
+
"skills":["lesson design","facilitation","marketing"],
|
| 29 |
+
"starter":["Pilot class","Testimonials","Cohorts","Bundles"]}
|
| 30 |
+
],
|
| 31 |
+
"education": [
|
| 32 |
+
{"path":"Tutoring niche","difficulty":2,"benefits":"Fast validation; referrals",
|
| 33 |
+
"skills":["subject mastery","scheduling","sales"],
|
| 34 |
+
"starter":["ICP","5 trial students","Packages","Referrals"]}
|
| 35 |
+
],
|
| 36 |
+
"fitness": [
|
| 37 |
+
{"path":"Online coaching","difficulty":2,"benefits":"Low overhead; trust",
|
| 38 |
+
"skills":["programming","accountability","content"],
|
| 39 |
+
"starter":["Niche offer","Clients 1–3","Testimonials","Packages"]}
|
| 40 |
+
],
|
| 41 |
+
"content": [
|
| 42 |
+
{"path":"Newsletter + paid tiers","difficulty":2,"benefits":"Audience compounds",
|
| 43 |
+
"skills":["editorial","conversion","email ops"],
|
| 44 |
+
"starter":["Niche POV","Weekly cadence","Lead magnet","Feedback loop"]}
|
| 45 |
+
],
|
| 46 |
+
"games": [
|
| 47 |
+
{"path":"Indie premium","difficulty":4,"benefits":"Creative control; wishlists matter",
|
| 48 |
+
"skills":["scope control","playtesting","marketing"],
|
| 49 |
+
"starter":["Core loop","Vertical slice","Store page","Playtests"]}
|
| 50 |
+
],
|
| 51 |
+
"fashion": [
|
| 52 |
+
{"path":"Print-on-demand niche","difficulty":2,"benefits":"Low inventory risk",
|
| 53 |
+
"skills":["design","niche research","ads"],
|
| 54 |
+
"starter":["Mockups","POD store","Ad test","Iterate"]}
|
| 55 |
+
]
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
SYSTEM_PROMPT = (
|
| 59 |
+
"You are a friendly tutor for Entrepreneurial Readiness and Paths. "
|
| 60 |
+
"Be practical and concise. If the user ran the assessment or path explorer, use that info."
|
| 61 |
+
)
|
| 62 |
+
|
| 63 |
+
# ------------------------------
|
| 64 |
+
# Readiness scoring (0–100)
|
| 65 |
+
# ------------------------------
|
| 66 |
+
def compute_readiness(payload):
|
| 67 |
+
# normalize
|
| 68 |
+
grit = payload["grit"]/10
|
| 69 |
+
risk = payload["risk"]/10
|
| 70 |
+
time_norm = min(payload["time_per_week"], 40)/40
|
| 71 |
+
runway_norm = min(payload["runway_months"], 24)/24
|
| 72 |
+
customer_access = payload["customer_access"]/10
|
| 73 |
+
validation_skill = payload["validation_skill"]/10
|
| 74 |
+
ops_skill = payload["ops_skill"]/10
|
| 75 |
+
finance_skill = payload["finance_skill"]/10
|
| 76 |
+
|
| 77 |
+
score = 100 * (
|
| 78 |
+
RUBRIC_WEIGHTS["grit"]*grit +
|
| 79 |
+
RUBRIC_WEIGHTS["risk"]*risk +
|
| 80 |
+
RUBRIC_WEIGHTS["time_per_week"]*time_norm +
|
| 81 |
+
RUBRIC_WEIGHTS["runway_months"]*runway_norm +
|
| 82 |
+
RUBRIC_WEIGHTS["customer_access"]*customer_access +
|
| 83 |
+
RUBRIC_WEIGHTS["validation_skill"]*validation_skill +
|
| 84 |
+
RUBRIC_WEIGHTS["ops_skill"]*ops_skill +
|
| 85 |
+
RUBRIC_WEIGHTS["finance_skill"]*finance_skill
|
| 86 |
+
)
|
| 87 |
+
score = int(round(score))
|
| 88 |
+
tier = "Starter" if score < 55 else ("Building" if score < 75 else "Launch-ready")
|
| 89 |
+
|
| 90 |
+
tips = []
|
| 91 |
+
if runway_norm < 0.35: tips.append("Build 6–12 months runway or run as a side project.")
|
| 92 |
+
if time_norm < 0.5: tips.append("Protect ~8–12 hrs/week for interviews & experiments.")
|
| 93 |
+
if customer_access < 0.6: tips.append("Line up 10–20 target users for interviews.")
|
| 94 |
+
if validation_skill < 0.6: tips.append("Run a demand test (landing page/preorder) before building.")
|
| 95 |
+
if finance_skill < 0.5: tips.append("Track cash runway monthly.")
|
| 96 |
+
if ops_skill < 0.5: tips.append("Create a weekly ops checklist to reduce chaos.")
|
| 97 |
+
return {"score":score, "tier":tier, "tips":tips}
|
| 98 |
+
|
| 99 |
+
# ------------------------------
|
| 100 |
+
# Path suggestions helper
|
| 101 |
+
# ------------------------------
|
| 102 |
+
def suggest_paths(interests, experience):
|
| 103 |
+
results = []
|
| 104 |
+
for domain in interests or []:
|
| 105 |
+
for item in PATHS.get(domain, []):
|
| 106 |
+
diff = item["difficulty"]
|
| 107 |
+
# adjust for experience
|
| 108 |
+
if experience == "beginner": diff = min(5, diff + 1)
|
| 109 |
+
if experience == "advanced": diff = max(1, diff - 1)
|
| 110 |
+
results.append({
|
| 111 |
+
"domain": domain,
|
| 112 |
+
"path": item["path"],
|
| 113 |
+
"difficulty_1to5": diff,
|
| 114 |
+
"benefits": item["benefits"],
|
| 115 |
+
"key_skills": item["skills"],
|
| 116 |
+
"starter_plan": item["starter"],
|
| 117 |
+
})
|
| 118 |
+
# keep it short
|
| 119 |
+
return results[:6]
|
| 120 |
+
|
| 121 |
+
# ------------------------------
|
| 122 |
+
# Simple rule-based "chat"
|
| 123 |
+
# ------------------------------
|
| 124 |
+
def local_reply(user_msg, history, assessment_state, path_state):
|
| 125 |
+
text = (user_msg or "").lower()
|
| 126 |
+
|
| 127 |
+
# reference readiness
|
| 128 |
+
if "score" in text or "ready" in text or "readiness" in text:
|
| 129 |
+
if isinstance(assessment_state, dict) and "score" in assessment_state:
|
| 130 |
+
s = assessment_state["score"]
|
| 131 |
+
t = assessment_state.get("tier", "?")
|
| 132 |
+
return f"Your readiness is **{s}/100 ({t})**. Top tip: {assessment_state.get('tips', ['Run small demand tests first.'])[0]}"
|
| 133 |
+
else:
|
| 134 |
+
return "Run **Assess readiness** on the right, then ask me again 🙂"
|
| 135 |
+
|
| 136 |
+
# reference paths
|
| 137 |
+
if "path" in text or "idea" in text or "domain" in text:
|
| 138 |
+
if isinstance(path_state, dict) and path_state.get("suggestions"):
|
| 139 |
+
sug = path_state["suggestions"][0]
|
| 140 |
+
return (f"Try **{sug['domain']} → {sug['path']}**. "
|
| 141 |
+
f"Difficulty ~{sug['difficulty_1to5']}/5. "
|
| 142 |
+
f"Benefits: {sug['benefits']}. "
|
| 143 |
+
f"First steps: {', '.join(sug['starter_plan'][:3])}. Want more options?")
|
| 144 |
+
else:
|
| 145 |
+
return "Pick a few interests under **Path Explorer**, hit **Suggest paths**, then ask me which to start with."
|
| 146 |
+
|
| 147 |
+
# detect domain mention
|
| 148 |
+
for domain in PATHS.keys():
|
| 149 |
+
if domain in text:
|
| 150 |
+
first = PATHS[domain][0]
|
| 151 |
+
return (f"In **{domain}**, consider **{first['path']}** "
|
| 152 |
+
f"(difficulty {first['difficulty']}/5). "
|
| 153 |
+
f"Why it can pay off: {first['benefits']}. "
|
| 154 |
+
f"Starter plan: {', '.join(first['starter'][:3])}.")
|
| 155 |
+
# default helpful tip
|
| 156 |
+
return ("Start with a small **demand test**: a landing page and 5–10 user interviews. "
|
| 157 |
+
"Measure real intent (signups/preorders) before you build.")
|
| 158 |
+
|
| 159 |
+
# ------------------------------
|
| 160 |
+
# Gradio UI
|
| 161 |
+
# ------------------------------
|
| 162 |
+
with gr.Blocks(theme="soft", fill_height=True) as demo:
|
| 163 |
+
gr.Markdown("## Entrepreneurial Tutor (no external model)\nAsk about readiness or paths. Use the tools on the right to feed me context.")
|
| 164 |
+
|
| 165 |
+
with gr.Row():
|
| 166 |
+
# Chat
|
| 167 |
+
with gr.Column(scale=3):
|
| 168 |
+
chat = gr.Chatbot(type="tuples", height=460)
|
| 169 |
+
with gr.Row():
|
| 170 |
+
msg = gr.Textbox(placeholder="Ask about Entrepreneurial paths or readiness…", scale=4)
|
| 171 |
+
send = gr.Button("Send", variant="primary")
|
| 172 |
+
|
| 173 |
+
# Tools
|
| 174 |
+
with gr.Column(scale=2):
|
| 175 |
+
gr.Markdown("### Quick Readiness Assessment")
|
| 176 |
+
grit = gr.Slider(0,10, value=6, step=1, label="Grit / persistence")
|
| 177 |
+
risk = gr.Slider(0,10, value=5, step=1, label="Comfort with risk")
|
| 178 |
+
time_per_week = gr.Slider(0,40, value=8, step=1, label="Time available per week (hrs)")
|
| 179 |
+
runway_months = gr.Slider(0,24, value=3, step=1, label="Financial runway (months)")
|
| 180 |
+
customer_access = gr.Slider(0,10, value=5, step=1, label="Access to target customers")
|
| 181 |
+
validation_skill = gr.Slider(0,10, value=4, step=1, label="Validation / testing skill")
|
| 182 |
+
ops_skill = gr.Slider(0,10, value=4, step=1, label="Operations / delivery skill")
|
| 183 |
+
finance_skill = gr.Slider(0,10, value=3, step=1, label="Finance / budgeting")
|
| 184 |
+
|
| 185 |
+
assess_btn = gr.Button("Assess readiness")
|
| 186 |
+
assessment_state = gr.State({})
|
| 187 |
+
assess_out = gr.JSON(label="Assessment Result")
|
| 188 |
+
|
| 189 |
+
gr.Markdown("### Path Explorer")
|
| 190 |
+
interests = gr.CheckboxGroup(
|
| 191 |
+
choices=list(PATHS.keys()),
|
| 192 |
+
value=["tech"],
|
| 193 |
+
label="Interests / domains"
|
| 194 |
+
)
|
| 195 |
+
experience = gr.Radio(
|
| 196 |
+
choices=["beginner","intermediate","advanced"],
|
| 197 |
+
value="beginner",
|
| 198 |
+
label="Experience level"
|
| 199 |
+
)
|
| 200 |
+
suggest_btn = gr.Button("Suggest paths")
|
| 201 |
+
path_state = gr.State({})
|
| 202 |
+
paths_out = gr.JSON(label="Suggested Paths")
|
| 203 |
+
|
| 204 |
+
# Wiring
|
| 205 |
+
def do_assess(grit, risk, time_per_week, runway_months, customer_access, validation_skill, ops_skill, finance_skill):
|
| 206 |
+
payload = {
|
| 207 |
+
"grit":grit,"risk":risk,"time_per_week":time_per_week,"runway_months":runway_months,
|
| 208 |
+
"customer_access":customer_access,"validation_skill":validation_skill,
|
| 209 |
+
"ops_skill":ops_skill,"finance_skill":finance_skill
|
| 210 |
+
}
|
| 211 |
+
result = compute_readiness(payload)
|
| 212 |
+
return result, result # show + store
|
| 213 |
+
|
| 214 |
+
assess_btn.click(
|
| 215 |
+
do_assess,
|
| 216 |
+
inputs=[grit, risk, time_per_week, runway_months, customer_access, validation_skill, ops_skill, finance_skill],
|
| 217 |
+
outputs=[assess_out, assessment_state]
|
| 218 |
+
)
|
| 219 |
+
|
| 220 |
+
def do_paths(interests, experience):
|
| 221 |
+
res = suggest_paths(interests, experience)
|
| 222 |
+
state = {"interests": interests, "experience": experience, "suggestions": res}
|
| 223 |
+
return res, state
|
| 224 |
+
|
| 225 |
+
suggest_btn.click(
|
| 226 |
+
do_paths,
|
| 227 |
+
inputs=[interests, experience],
|
| 228 |
+
outputs=[paths_out, path_state]
|
| 229 |
+
)
|
| 230 |
+
|
| 231 |
+
def on_send(user_message, history, assessment_state, path_state):
|
| 232 |
+
if not user_message:
|
| 233 |
+
return gr.update(), history
|
| 234 |
+
reply = local_reply(user_message, history, assessment_state, path_state)
|
| 235 |
+
return "", (history or []) + [[user_message, reply]]
|
| 236 |
+
|
| 237 |
+
send.click(
|
| 238 |
+
on_send,
|
| 239 |
+
inputs=[msg, chat, assessment_state, path_state],
|
| 240 |
+
outputs=[msg, chat]
|
| 241 |
+
)
|
| 242 |
+
|
| 243 |
+
if __name__ == "__main__":
|
| 244 |
+
demo.launch()
|