feat: организационный слой — оргструктура + должностные инструкции (между IDEF0 и ТЗ)

- backend: генераторы build-orgchart (из IDEF0 mechanisms) и build-jobs
  (должностные с учётом отклонений клиента), артефакты orgchart/jobs в state
- кабинет: вкладка «🏢 Организация» на этапе 4 — оргструктура (роли, штат,
  подчинённость, узкие места) + должностные (ответственность, KPI, полномочия)
This commit is contained in:
wasrusgen 2026-06-01 23:48:34 +03:00
parent 940cf3484f
commit 149f02da37
2 changed files with 126 additions and 3 deletions

View File

@ -464,6 +464,89 @@ def ask():
con.commit() con.commit()
return jsonify({"reply": reply, "deviation_recorded": recorded}) 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 + декомпозиция) ── # ── Tool schema: строгий IDEF0 (ICOM + декомпозиция) ──
ARROW = { ARROW = {
"type": "object", "type": "object",
@ -1347,6 +1430,8 @@ def get_project_state(token):
"model": json.loads(model_row["blocks_json"]) if model_row else None, "model": json.loads(model_row["blocks_json"]) if model_row else None,
"selection": latest_artifact(proj["id"], "selection"), "selection": latest_artifact(proj["id"], "selection"),
"canvas": latest_artifact(proj["id"], "canvas"), "canvas": latest_artifact(proj["id"], "canvas"),
"orgchart": latest_artifact(proj["id"], "orgchart"),
"jobs": latest_artifact(proj["id"], "jobs"),
"spec": latest_artifact(proj["id"], "spec"), "spec": latest_artifact(proj["id"], "spec"),
"approvals": latest_artifact(proj["id"], "approvals") or {}, "approvals": latest_artifact(proj["id"], "approvals") or {},
"crm": crm, "crm": crm,

View File

@ -598,7 +598,7 @@ async function sendMsg(){
let anTab="canvas"; let anTab="canvas";
function renderAnalysis(){ function renderAnalysis(){
const pad=document.getElementById("anPad"); const pad=document.getElementById("anPad");
pad.innerHTML=`<div class="an-tabs"><div class="an-tab ${anTab==='canvas'?'active':''}" onclick="setAnTab('canvas')">📊 Стратегия</div><div class="an-tab ${anTab==='idef0'?'active':''}" onclick="setAnTab('idef0')">🔧 Функции</div></div><div id="anContent"></div>`; pad.innerHTML=`<div class="an-tabs"><div class="an-tab ${anTab==='canvas'?'active':''}" onclick="setAnTab('canvas')">📊 Стратегия</div><div class="an-tab ${anTab==='idef0'?'active':''}" onclick="setAnTab('idef0')">🔧 Функции</div><div class="an-tab ${anTab==='org'?'active':''}" onclick="setAnTab('org')">🏢 Организация</div></div><div id="anContent"></div>`;
renderAnContent(); renderAnContent();
} }
function setAnTab(t){anTab=t;renderAnalysis()} function setAnTab(t){anTab=t;renderAnalysis()}
@ -607,11 +607,49 @@ function renderAnContent(){
if(anTab==='canvas'){ if(anTab==='canvas'){
if(!state.canvas){c.innerHTML=runCard("canvas","📊","Стратегическая модель","Елена построит Business Model Canvas — как устроен ваш бизнес и как он зарабатывает.","Построить стратегию →");return} if(!state.canvas){c.innerHTML=runCard("canvas","📊","Стратегическая модель","Елена построит Business Model Canvas — как устроен ваш бизнес и как он зарабатывает.","Построить стратегию →");return}
c.innerHTML=renderCanvas(state.canvas); c.innerHTML=renderCanvas(state.canvas);
}else{ }else if(anTab==='idef0'){
if(!state.model){c.innerHTML=runCard("model","🔧","Функциональная модель","Елена разложит бизнес на функции (IDEF0): входы, выходы, нормы, ресурсы и разрывы.","Построить модель →");return} if(!state.model){c.innerHTML=runCard("model","🔧","Функциональная модель","Елена разложит бизнес на функции (IDEF0): входы, выходы, нормы, ресурсы и разрывы.","Построить модель →");return}
c.innerHTML=renderIdef(state.model); 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+=`<div style="height:14px"></div>`+runCard("jobs","📋","Должностные инструкции","По ролям: зоны ответственности, KPI, полномочия. С учётом ваших пожеланий (отклонений).","Собрать инструкции →");
else h+=`<div style="height:14px"></div>`+renderJobs(state.jobs);
}
return h;
}
function renderOrgChart(o){
const units=o.units||[];
let h=`<div style="background:var(--sb);color:#fff;border-radius:11px;padding:12px 16px;margin-bottom:14px;font-size:13px"><b style="color:var(--mid)">Вывод:</b> ${esc(o.insight||'')}</div>`;
h+=units.map(u=>`<div style="background:var(--white);border:1px solid var(--border);border-radius:11px;padding:12px 14px;margin-bottom:8px">
<div style="display:flex;align-items:center;gap:9px;flex-wrap:wrap">
<span style="font-size:14px;font-weight:800">${esc(u.role||'')}</span>
<span style="font-size:11px;font-weight:700;color:#047857;background:#D1FAE5;padding:1px 8px;border-radius:5px">${(+u.headcount||1)} чел.</span>
${u.reports_to&&u.reports_to!=='—'?`<span style="font-size:11px;color:#9ca3af">↑ ${esc(u.reports_to)}</span>`:'<span style="font-size:10px;font-weight:700;color:#6366F1;background:#EEF2FF;padding:1px 8px;border-radius:5px">руководство</span>'}
</div>
${(u.owns_functions&&u.owns_functions.length)?`<div style="font-size:11px;color:#6b7280;margin-top:6px">Отвечает: ${u.owns_functions.map(f=>`<span style="background:#F3F4F6;padding:1px 6px;border-radius:4px;margin-right:3px;display:inline-block">${esc(f)}</span>`).join('')}</div>`:''}
${u.note?`<div style="font-size:11px;color:#92400E;background:#FEF3C7;border-radius:6px;padding:5px 9px;margin-top:6px">⚠ ${esc(u.note)}</div>`:''}
</div>`).join('');
return h;
}
function renderJobs(j){
const roles=j.roles||[];
return `<div style="font-size:13px;font-weight:800;margin-bottom:10px">📋 Должностные инструкции</div>`+roles.map(r=>`<div style="background:var(--white);border:1px solid var(--border);border-radius:11px;padding:14px 16px;margin-bottom:10px">
<div style="font-size:14px;font-weight:800">${esc(r.role||'')}</div>
<div style="font-size:12px;color:#6b7280;margin:3px 0 9px">${esc(r.purpose||'')}${r.reports_to?` · ↑ ${esc(r.reports_to)}`:''}</div>
${(r.responsibilities&&r.responsibilities.length)?`<div style="font-size:11px;font-weight:700;color:#374151;margin-bottom:3px">Зоны ответственности</div><ul style="margin:0 0 9px;padding-left:18px;font-size:12px;color:#4B5563">${r.responsibilities.map(x=>`<li>${esc(x)}</li>`).join('')}</ul>`:''}
${(r.kpis&&r.kpis.length)?`<div style="font-size:11px;font-weight:700;color:#047857;margin-bottom:3px">KPI</div><div style="display:flex;flex-wrap:wrap;gap:5px;margin-bottom:9px">${r.kpis.map(k=>`<span style="font-size:11px;background:#ECFDF5;color:#047857;padding:2px 8px;border-radius:5px">${esc(k)}</span>`).join('')}</div>`:''}
${(r.authority&&r.authority.length)?`<div style="font-size:11px;color:#6b7280">Полномочия: ${r.authority.map(a=>esc(a)).join(' · ')}</div>`:''}
${r.deviation_note?`<div style="font-size:11px;color:#92400E;background:#FEF3C7;border-radius:6px;padding:5px 9px;margin-top:8px">⚠ Учтено пожелание клиента: ${esc(r.deviation_note)}</div>`:''}
</div>`).join('');
}
function renderSpecPane(){ function renderSpecPane(){
const pad=document.getElementById("specPad"); const pad=document.getElementById("specPad");
if(!state.spec){ if(!state.spec){
@ -703,7 +741,7 @@ function runCard(stage,ic,t,d,btn,custom){
const onclick=custom?'':`onclick="runBuild('${stage}')"`; const onclick=custom?'':`onclick="runBuild('${stage}')"`;
return `<div class="run-card"><div class="run-ic">${ic}</div><div class="run-t">${t}</div><div class="run-d">${d}</div><button class="btn btn-p" ${id} ${custom?'':onclick}>${btn}</button></div>`; return `<div class="run-card"><div class="run-ic">${ic}</div><div class="run-t">${t}</div><div class="run-d">${d}</div><button class="btn btn-p" ${id} ${custom?'':onclick}>${btn}</button></div>`;
} }
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){ async function runBuild(stage){
const [ep,key]=BUILD[stage];const btn=document.getElementById(`rb-${stage}`); const [ep,key]=BUILD[stage];const btn=document.getElementById(`rb-${stage}`);
if(btn){btn.disabled=true;btn.innerHTML=`<span class="spin"></span> Елена анализирует...`} if(btn){btn.disabled=true;btn.innerHTML=`<span class="spin"></span> Елена анализирует...`}