junaid12kh's picture
Upload 17 files
5b1ccd7 verified
from __future__ import annotations
from typing import Any
from fastapi.responses import HTMLResponse
from fastapi import FastAPI
import uvicorn
from openenv.core.env_server import create_fastapi_app
from logging_utils import get_logger
from .environment import CareerAction, CareerEnvironment, CareerObservation, CareerState
openenv_app: FastAPI = create_fastapi_app(
CareerEnvironment,
action_cls=CareerAction,
observation_cls=CareerObservation,
)
logger = get_logger("server")
app = FastAPI(title="AI Career Advisor")
app.mount("/openenv", openenv_app)
UI_HTML = """
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>AI Career Advisor</title>
<style>
:root {
--bg: #090d16;
--panel: #1c212c;
--panel-border: #3a404f;
--text: #f8fafc;
--muted: #b4bccb;
--primary: #c7440d;
--primary-hover: #e45517;
--table-row: #111827;
--chip: #0b1220;
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: "Segoe UI", "Helvetica Neue", Arial, sans-serif;
background: linear-gradient(160deg, #070a13 0%, #0a1020 50%, #0a0f1a 100%);
color: var(--text);
min-height: 100vh;
}
.wrap {
width: min(1120px, 94vw);
margin: 24px auto 18px;
}
h1 {
margin: 0 0 10px 0;
font-size: 38px;
line-height: 1.1;
letter-spacing: 0.3px;
}
.intro {
margin: 0 0 16px 0;
color: var(--muted);
font-size: 15px;
}
.intro code {
color: #f5f5f5;
background: #101726;
border: 1px solid #2e3646;
padding: 2px 6px;
border-radius: 6px;
}
.card {
background: linear-gradient(90deg, rgba(39, 45, 60, 0.92), rgba(33, 38, 49, 0.92));
border: 1px solid var(--panel-border);
border-radius: 8px;
padding: 12px;
margin-bottom: 12px;
}
.label {
display: block;
font-weight: 600;
margin-bottom: 10px;
color: #e5e7eb;
}
textarea {
width: 100%;
min-height: 66px;
resize: vertical;
border-radius: 6px;
border: 1px solid #3a4150;
background: #232833;
color: var(--text);
padding: 10px;
font-size: 15px;
outline: none;
}
textarea:focus {
border-color: #56617a;
box-shadow: 0 0 0 2px rgba(103, 126, 178, 0.2);
}
.btn {
width: 100%;
border: 0;
border-radius: 7px;
background: var(--primary);
color: #fff;
font-weight: 700;
font-size: 20px;
line-height: 1;
padding: 14px 12px;
margin: 4px 0 12px;
cursor: pointer;
transition: background 0.15s ease;
}
.btn:hover { background: var(--primary-hover); }
.btn:disabled {
background: #6b7280;
cursor: not-allowed;
}
.section-title {
margin: 16px 0 8px;
font-size: 22px;
font-weight: 700;
}
.out {
min-height: 126px;
white-space: pre-wrap;
line-height: 1.45;
font-size: 15px;
}
.table-wrap {
overflow-x: auto;
border-radius: 8px;
border: 1px solid #2b3342;
}
table {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
font-size: 14px;
background: #0a111f;
}
th, td {
border: 1px solid #2b3342;
padding: 10px;
text-align: left;
vertical-align: top;
word-wrap: break-word;
}
th {
background: #0b1220;
font-weight: 700;
color: #f3f4f6;
}
tbody tr:nth-child(odd) { background: var(--table-row); }
.examples {
margin-top: 14px;
color: #e5e7eb;
font-size: 14px;
}
.chips {
margin-top: 10px;
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.chip {
padding: 8px 12px;
border-radius: 10px;
border: 1px solid #334155;
background: var(--chip);
color: #fff;
cursor: pointer;
font-size: 14px;
}
.footer {
text-align: center;
color: #9ca3af;
font-size: 14px;
margin: 40px 0 10px;
}
@media (max-width: 900px) {
.wrap { width: 95vw; margin-top: 16px; }
h1 { font-size: 30px; }
.intro { font-size: 14px; }
.btn { font-size: 18px; padding: 12px 10px; }
.section-title { font-size: 20px; }
.out { font-size: 14px; }
table { font-size: 13px; }
}
</style>
</head>
<body>
<div class="wrap">
<h1>AI Career Advisor</h1>
<p class="intro">
Enter your current skills to get a career recommendation and simulation.
OpenEnv routes stay available at <code>/openenv/reset</code>, <code>/openenv/step</code>, and <code>/openenv/state</code>.
</p>
<div class="card">
<label class="label" for="skillsInput">Current skills</label>
<textarea id="skillsInput" placeholder="python, excel"></textarea>
</div>
<button id="runBtn" class="btn" disabled>Run simulation</button>
<div class="card">
<div class="label">Career recommendation</div>
<div id="recommendation" class="out"></div>
</div>
<div class="section-title">Top career matches</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>role</th>
<th>current fit</th>
<th>matched skills</th>
<th>missing skills</th>
<th>base salary</th>
<th>market demand</th>
</tr>
</thead>
<tbody id="matchesBody"></tbody>
</table>
</div>
<div class="section-title">Simulation steps</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>step</th>
<th>action</th>
<th>career</th>
<th>salary</th>
<th>reward</th>
<th>feedback</th>
</tr>
</thead>
<tbody id="stepsBody"></tbody>
</table>
</div>
<div class="examples">
<div>Examples</div>
<div class="chips">
<button class="chip" data-example="python">python</button>
<button class="chip" data-example="python, machine learning">python, machine learning</button>
<button class="chip" data-example="python, statistics">python, statistics</button>
<button class="chip" data-example="python, statistics, excel">python, statistics, excel</button>
<button class="chip" data-example="excel">excel</button>
<button class="chip" data-example="excel, communication">excel, communication</button>
</div>
</div>
<div class="footer">Built with FastAPI HTML UI</div>
</div>
<script>
const CAREERS = [
{
role: "AI Engineer",
skills: ["python", "machine learning"],
baseSalary: 90000,
demand: 95,
},
{
role: "Data Analyst",
skills: ["python", "statistics", "excel"],
baseSalary: 60000,
demand: 75,
},
{
role: "Accountant",
skills: ["excel", "communication"],
baseSalary: 45000,
demand: 55,
},
];
const BONUS = [0.95, 1.077, 1.081, 1.081, 1.09, 1.089];
const skillsInput = document.getElementById("skillsInput");
const recommendation = document.getElementById("recommendation");
const matchesBody = document.getElementById("matchesBody");
const stepsBody = document.getElementById("stepsBody");
const runBtn = document.getElementById("runBtn");
function setEmptyState() {
recommendation.textContent = "Enter skills and click Run simulation to see recommendation.";
matchesBody.innerHTML = '<tr><td colspan="6">No results yet. Run simulation to populate career matches.</td></tr>';
stepsBody.innerHTML = '<tr><td colspan="6">No simulation yet. Add skills and click Run simulation.</td></tr>';
}
function updateRunButtonState() {
runBtn.disabled = !skillsInput.value.trim();
}
function parseSkills(text) {
return [...new Set(
(text || "")
.split(",")
.map((x) => x.trim().toLowerCase())
.filter(Boolean)
)];
}
function fmtMoney(value) {
return "$" + Math.round(value).toLocaleString("en-US");
}
function pct(n) {
return `${Math.round(n)}%`;
}
function titleCase(skill) {
return skill
.split(" ")
.map((s) => s.charAt(0).toUpperCase() + s.slice(1))
.join(" ");
}
function buildMatches(userSkills) {
return CAREERS.map((career) => {
const matched = career.skills.filter((s) => userSkills.includes(s));
const missing = career.skills.filter((s) => !userSkills.includes(s));
const fit = career.skills.length ? (matched.length / career.skills.length) * 100 : 0;
return {
role: career.role,
fit,
matched,
missing,
baseSalary: career.baseSalary,
demand: career.demand,
};
}).sort((a, b) => (b.fit + b.demand / 10) - (a.fit + a.demand / 10));
}
function renderMatchesTable(rows) {
matchesBody.innerHTML = rows.map((r) => {
return `<tr>
<td>${r.role}</td>
<td>${pct(r.fit)}</td>
<td>${r.matched.length ? r.matched.map(titleCase).join(", ") : "None yet"}</td>
<td>${r.missing.length ? r.missing.map(titleCase).join(", ") : "Ready now"}</td>
<td>${fmtMoney(r.baseSalary)}</td>
<td>${pct(r.demand)}</td>
</tr>`;
}).join("");
}
function runSalarySim(bestCareer) {
const steps = [];
let salary = 0;
for (let i = 0; i < BONUS.length; i++) {
if (i === 0) {
steps.push({
step: 1,
action: `Choose career: ${bestCareer.role}`,
career: bestCareer.role,
salary: 0,
reward: BONUS[0],
feedback: "Good",
});
} else {
const gain = (bestCareer.baseSalary / 12) * BONUS[i];
salary += gain;
steps.push({
step: i + 1,
action: "Work in role",
career: bestCareer.role,
salary,
reward: BONUS[i],
feedback: "Good",
});
}
}
return steps;
}
function renderStepsTable(steps) {
stepsBody.innerHTML = steps.map((s) => {
return `<tr>
<td>${s.step}</td>
<td>${s.action}</td>
<td>${s.career}</td>
<td>${fmtMoney(s.salary)}</td>
<td>${s.reward}</td>
<td>${s.feedback}</td>
</tr>`;
}).join("");
}
function buildSummary(userSkills, topRows, steps) {
const best = topRows[0];
const nextBest = topRows[1];
const finalSalary = steps[steps.length - 1].salary;
const missingText = best.missing.length ? best.missing.map(titleCase).join(", ") : "You already meet the listed skills";
return [
`Best fit today: ${best.role}`,
`Why this role: ${pct(best.fit)} skill match and ${pct(best.demand)} market demand in this demo.`,
`Skills you already have: ${best.matched.length ? best.matched.map(titleCase).join(", ") : "None yet"}`,
`Top gaps to close: ${missingText}`,
`Simulated earnings after ramp-up: ${fmtMoney(finalSalary)}`,
`Updated skill profile: ${userSkills.length ? userSkills.map(titleCase).join(", ") : "None listed"}`,
`Next best option: ${nextBest ? nextBest.role : "N/A"}`,
`Simulation result: Good`,
].join("\\n");
}
async function callOpenEnvRoutes(skillText) {
try {
await fetch("/openenv/reset", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: "{}",
});
} catch (_) {}
try {
await fetch("/openenv/step", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ action: { query: skillText } }),
});
} catch (_) {}
}
async function run() {
const raw = skillsInput.value.trim();
if (!raw) {
setEmptyState();
updateRunButtonState();
return;
}
const userSkills = parseSkills(raw);
const rows = buildMatches(userSkills);
const steps = runSalarySim(rows[0]);
renderMatchesTable(rows);
renderStepsTable(steps);
recommendation.textContent = buildSummary(userSkills, rows, steps);
await callOpenEnvRoutes(raw || "career guidance");
}
runBtn.addEventListener("click", run);
skillsInput.addEventListener("input", updateRunButtonState);
document.querySelectorAll(".chip").forEach((chip) => {
chip.addEventListener("click", () => {
skillsInput.value = chip.dataset.example || "";
updateRunButtonState();
});
});
setEmptyState();
updateRunButtonState();
</script>
</body>
</html>
"""
@app.get("/", response_class=HTMLResponse)
def root() -> HTMLResponse:
logger.info("UI root endpoint hit.")
return HTMLResponse(content=UI_HTML)
@app.api_route("/reset", methods=["GET", "POST"])
def reset_compat(body: dict[str, Any] | None = None) -> dict[str, Any]:
payload = body or {}
env = CareerEnvironment()
obs = env.reset(
seed=payload.get("seed"),
episode_id=payload.get("episode_id"),
)
return {
"observation": obs.model_dump(),
"reward": obs.reward,
"done": obs.done,
}
@app.api_route("/state", methods=["GET", "POST"])
def state_compat() -> dict[str, Any]:
env = CareerEnvironment()
return env.state.model_dump()
@app.get("/schema")
def schema_compat() -> dict[str, Any]:
return {
"action": CareerAction.model_json_schema(),
"observation": CareerObservation.model_json_schema(),
"state": CareerState.model_json_schema(),
}
@app.api_route("/step", methods=["POST"])
def step_compat(body: dict[str, Any]) -> dict[str, Any]:
action_payload = body.get("action")
if isinstance(action_payload, dict):
query = str(action_payload.get("query", ""))
else:
query = str(body.get("query", ""))
env = CareerEnvironment()
obs = env.step(CareerAction(query=query), timeout_s=body.get("timeout_s"))
return {
"observation": obs.model_dump(),
"reward": obs.reward,
"done": obs.done,
}
def main() -> None:
logger.info("Starting uvicorn server.app:app on 0.0.0.0:8000")
uvicorn.run("server.app:app", host="0.0.0.0", port=8000)
if __name__ == "__main__":
main()