diff --git a/backend/elena_app.py b/backend/elena_app.py index 68bab37..bf83c0d 100644 --- a/backend/elena_app.py +++ b/backend/elena_app.py @@ -464,6 +464,89 @@ def ask(): con.commit() return jsonify({"reply": reply, "deviation_recorded": recorded}) +# ── Организационный слой: оргструктура + должностные инструкции (между IDEF0 и ТЗ) ── +ORGCHART_TOOL = { + "name": "build_orgchart", + "description": "Строит целевую оргструктуру (TO-BE) из функциональной модели и интервью.", + "input_schema": { + "type": "object", + "properties": { + "units": {"type": "array", "items": {"type": "object", "properties": { + "role": {"type": "string", "description": "Должность/роль"}, + "reports_to": {"type": "string", "description": "Кому подчиняется (роль) или '—' для верхнего уровня"}, + "headcount": {"type": "integer", "description": "Сколько человек на этой роли"}, + "owns_functions": {"type": "array", "items": {"type": "string"}, "description": "Функции IDEF0 (node_id или название), за которые отвечает"}, + "note": {"type": "string", "description": "Комментарий: совмещения, особенности, конфликт интересов"} + }, "required": ["role", "reports_to", "headcount"]}}, + "insight": {"type": "string", "description": "Ключевой вывод: узкие места, перегруз, конфликты интересов в структуре"} + }, + "required": ["units", "insight"] + } +} +ORG_INSTRUCTION = """Построй целевую оргструктуру (TO-BE) бизнеса клиента. +Опирайся на функциональную модель (mechanisms — кто выполняет функции) и интервью. +Учитывай реальный масштаб — не раздувай штат. Где есть совмещения ролей или конфликт интересов (один человек и исполняет, и контролирует) — отметь в note соответствующей роли и в insight. +Если зафиксированы отклонения клиента — учитывай их (например, совмещение склада и пошива оставлено по требованию клиента). +Вызови build_orgchart.""" + +@app.route("/api/build-orgchart", methods=["POST"]) +def build_orgchart_route(): + data = request.get_json(force=True) or {} + proj = get_project(data.get("token")) + if not proj: + return jsonify({"error": "not found"}), 404 + extra = stage_artifact_context(proj["id"], "idef0") # canvas + idef0 + отклонения + result, usage = run_tool(proj["id"], ORGCHART_TOOL, "build_orgchart", ORG_INSTRUCTION, extra_context=extra, max_tokens=2500) + if result is None: + return jsonify({"error": usage}), 500 + save_artifact(proj["id"], "orgchart", result) + return jsonify({"orgchart": result, "usage": usage}) + +JOBS_TOOL = { + "name": "build_job_descriptions", + "description": "Должностные инструкции по ролям из оргструктуры и функций, с учётом отклонений клиента.", + "input_schema": { + "type": "object", + "properties": { + "roles": {"type": "array", "items": {"type": "object", "properties": { + "role": {"type": "string"}, + "purpose": {"type": "string", "description": "Цель должности одним предложением"}, + "responsibilities": {"type": "array", "items": {"type": "string"}, "description": "Зоны ответственности"}, + "kpis": {"type": "array", "items": {"type": "string"}, "description": "Показатели эффективности (измеримые)"}, + "reports_to": {"type": "string", "description": "Кому подчиняется"}, + "authority": {"type": "array", "items": {"type": "string"}, "description": "Права и полномочия"}, + "deviation_note": {"type": "string", "description": "Если роль затронута отклонением клиента — как именно (совмещение и риск)"} + }, "required": ["role", "purpose", "responsibilities", "kpis"]}} + }, + "required": ["roles"] + } +} +JOBS_INSTRUCTION = """Составь должностные инструкции по ролям из целевой оргструктуры и функциональной модели. +Для каждой роли: цель должности, зоны ответственности, измеримые KPI, кому подчиняется, права/полномочия. +ВАЖНО: если роль затронута отклонением клиента (совмещение функций и т.п.) — отрази это честно в deviation_note и в обязанностях, с оговоркой про риск (например, совмещение склада и пошива — риск «плавающего» учёта остатков). +Вызови build_job_descriptions.""" + +@app.route("/api/build-jobs", methods=["POST"]) +def build_jobs_route(): + data = request.get_json(force=True) or {} + proj = get_project(data.get("token")) + if not proj: + return jsonify({"error": "not found"}), 404 + pid = proj["id"] + parts = [] + org = latest_artifact(pid, "orgchart") + if org: + parts.append("ЦЕЛЕВАЯ ОРГСТРУКТУРА:\n" + json.dumps(org, ensure_ascii=False)[:3000]) + art = stage_artifact_context(pid, "idef0") # idef0 + canvas + отклонения + if art: + parts.append(art) + extra = "\n\n".join(parts) if parts else None + result, usage = run_tool(pid, JOBS_TOOL, "build_job_descriptions", JOBS_INSTRUCTION, extra_context=extra, max_tokens=3500) + if result is None: + return jsonify({"error": usage}), 500 + save_artifact(pid, "jobs", result) + return jsonify({"jobs": result, "usage": usage}) + # ── Tool schema: строгий IDEF0 (ICOM + декомпозиция) ── ARROW = { "type": "object", @@ -1347,6 +1430,8 @@ def get_project_state(token): "model": json.loads(model_row["blocks_json"]) if model_row else None, "selection": latest_artifact(proj["id"], "selection"), "canvas": latest_artifact(proj["id"], "canvas"), + "orgchart": latest_artifact(proj["id"], "orgchart"), + "jobs": latest_artifact(proj["id"], "jobs"), "spec": latest_artifact(proj["id"], "spec"), "approvals": latest_artifact(proj["id"], "approvals") or {}, "crm": crm, diff --git a/docs/cabinet.html b/docs/cabinet.html index 2b42eca..3140cf4 100644 --- a/docs/cabinet.html +++ b/docs/cabinet.html @@ -598,7 +598,7 @@ async function sendMsg(){ let anTab="canvas"; function renderAnalysis(){ const pad=document.getElementById("anPad"); - pad.innerHTML=`
📊 Стратегия
🔧 Функции
`; + pad.innerHTML=`
📊 Стратегия
🔧 Функции
🏢 Организация
`; renderAnContent(); } function setAnTab(t){anTab=t;renderAnalysis()} @@ -607,11 +607,49 @@ function renderAnContent(){ if(anTab==='canvas'){ if(!state.canvas){c.innerHTML=runCard("canvas","📊","Стратегическая модель","Елена построит Business Model Canvas — как устроен ваш бизнес и как он зарабатывает.","Построить стратегию →");return} c.innerHTML=renderCanvas(state.canvas); - }else{ + }else if(anTab==='idef0'){ if(!state.model){c.innerHTML=runCard("model","🔧","Функциональная модель","Елена разложит бизнес на функции (IDEF0): входы, выходы, нормы, ресурсы и разрывы.","Построить модель →");return} c.innerHTML=renderIdef(state.model); + }else{ + c.innerHTML=renderOrg(); } } +function renderOrg(){ + if(!state.model)return runCard(null,"⚠️","Сначала функции","Оргструктура строится из функциональной модели. Постройте модель на вкладке «Функции».","→ К функциям",()=>setAnTab('idef0')); + let h=''; + if(!state.orgchart)h+=runCard("orgchart","🏢","Целевая оргструктура","Елена построит оргструктуру из модели: кто за что отвечает, подчинённость, штат, узкие места.","Построить оргструктуру →"); + else h+=renderOrgChart(state.orgchart); + if(state.orgchart){ + if(!state.jobs)h+=`
`+runCard("jobs","📋","Должностные инструкции","По ролям: зоны ответственности, KPI, полномочия. С учётом ваших пожеланий (отклонений).","Собрать инструкции →"); + else h+=`
`+renderJobs(state.jobs); + } + return h; +} +function renderOrgChart(o){ + const units=o.units||[]; + let h=`
Вывод: ${esc(o.insight||'')}
`; + h+=units.map(u=>`
+
+ ${esc(u.role||'')} + ${(+u.headcount||1)} чел. + ${u.reports_to&&u.reports_to!=='—'?`↑ ${esc(u.reports_to)}`:'руководство'} +
+ ${(u.owns_functions&&u.owns_functions.length)?`
Отвечает: ${u.owns_functions.map(f=>`${esc(f)}`).join('')}
`:''} + ${u.note?`
⚠ ${esc(u.note)}
`:''} +
`).join(''); + return h; +} +function renderJobs(j){ + const roles=j.roles||[]; + return `
📋 Должностные инструкции
`+roles.map(r=>`
+
${esc(r.role||'')}
+
${esc(r.purpose||'')}${r.reports_to?` · ↑ ${esc(r.reports_to)}`:''}
+ ${(r.responsibilities&&r.responsibilities.length)?`
Зоны ответственности
`:''} + ${(r.kpis&&r.kpis.length)?`
KPI
${r.kpis.map(k=>`${esc(k)}`).join('')}
`:''} + ${(r.authority&&r.authority.length)?`
Полномочия: ${r.authority.map(a=>esc(a)).join(' · ')}
`:''} + ${r.deviation_note?`
⚠ Учтено пожелание клиента: ${esc(r.deviation_note)}
`:''} +
`).join(''); +} function renderSpecPane(){ const pad=document.getElementById("specPad"); if(!state.spec){ @@ -703,7 +741,7 @@ function runCard(stage,ic,t,d,btn,custom){ const onclick=custom?'':`onclick="runBuild('${stage}')"`; return `
${ic}
${t}
${d}
`; } -const BUILD={canvas:["build-canvas","canvas"],model:["build-model","model"],spec:["build-spec","spec"]}; +const BUILD={canvas:["build-canvas","canvas"],model:["build-model","model"],spec:["build-spec","spec"],orgchart:["build-orgchart","orgchart"],jobs:["build-jobs","jobs"]}; async function runBuild(stage){ const [ep,key]=BUILD[stage];const btn=document.getElementById(`rb-${stage}`); if(btn){btn.disabled=true;btn.innerHTML=` Елена анализирует...`}