kin/core/phases.py

244 lines
8.1 KiB
Python
Raw Normal View History

"""
Kin Research Phase Pipeline (KIN-059).
Sequential workflow: Director describes a new project, picks researcher roles,
each phase produces a task for review. After approve next phase activates.
Architect always runs last (auto-added when any researcher is selected).
"""
import sqlite3
from core import models
2026-03-19 19:06:18 +02:00
# Canonical order of research roles (knowledge_synthesizer and architect always last)
RESEARCH_ROLES = [
"business_analyst",
"market_researcher",
"legal_researcher",
"tech_researcher",
"ux_designer",
"marketer",
2026-03-19 19:06:18 +02:00
"knowledge_synthesizer",
"architect",
]
# Human-readable labels
ROLE_LABELS = {
"business_analyst": "Business Analyst",
"market_researcher": "Market Researcher",
"legal_researcher": "Legal Researcher",
"tech_researcher": "Tech Researcher",
"ux_designer": "UX Designer",
"marketer": "Marketer",
2026-03-19 19:06:18 +02:00
"knowledge_synthesizer": "Knowledge Synthesizer",
"architect": "Architect",
}
def validate_roles(roles: list[str]) -> list[str]:
2026-03-19 19:06:18 +02:00
"""Filter unknown roles, remove duplicates, strip auto-managed roles (architect, knowledge_synthesizer)."""
seen: set[str] = set()
result = []
for r in roles:
r = r.strip().lower()
2026-03-19 19:06:18 +02:00
if r in ("architect", "knowledge_synthesizer"):
continue
if r in RESEARCH_ROLES and r not in seen:
seen.add(r)
result.append(r)
return result
def build_phase_order(selected_roles: list[str]) -> list[str]:
2026-03-19 19:06:18 +02:00
"""Return roles in canonical RESEARCH_ROLES order.
Auto-inserts knowledge_synthesizer before architect when 2 researchers selected.
Architect always appended last when any researcher is selected.
"""
ordered = [
r for r in RESEARCH_ROLES
if r in selected_roles and r not in ("architect", "knowledge_synthesizer")
]
if ordered:
2026-03-19 19:06:18 +02:00
if len(ordered) >= 2:
ordered.append("knowledge_synthesizer")
ordered.append("architect")
return ordered
def create_project_with_phases(
conn: sqlite3.Connection,
id: str,
name: str,
path: str | None = None,
*,
description: str,
selected_roles: list[str],
tech_stack: list | None = None,
priority: int = 5,
language: str = "ru",
) -> dict:
"""Create project + sequential research phases.
Returns {project, phases}.
"""
clean_roles = validate_roles(selected_roles)
ordered_roles = build_phase_order(clean_roles)
if not ordered_roles:
raise ValueError("At least one research role must be selected")
2026-03-19 19:25:38 +02:00
# knowledge_synthesizer is included in build_phase_order output for routing/context,
# but is not yet a pipeline phase — it activates via separate aggregation trigger
pipeline_roles = [r for r in ordered_roles if r != "knowledge_synthesizer"]
project = models.create_project(
conn, id, name, path,
tech_stack=tech_stack, priority=priority, language=language,
description=description,
)
phases = []
2026-03-19 19:25:38 +02:00
for idx, role in enumerate(pipeline_roles):
phase = models.create_phase(conn, id, role, idx)
phases.append(phase)
# Activate the first phase immediately
if phases:
phases[0] = activate_phase(conn, phases[0]["id"])
return {"project": project, "phases": phases}
def activate_phase(conn: sqlite3.Connection, phase_id: int) -> dict:
"""Create a task for the phase and set it to active.
Task brief includes project description + phase context.
"""
phase = models.get_phase(conn, phase_id)
if not phase:
raise ValueError(f"Phase {phase_id} not found")
project = models.get_project(conn, phase["project_id"])
if not project:
raise ValueError(f"Project {phase['project_id']} not found")
task_id = models.next_task_id(conn, phase["project_id"], category=None)
brief = {
"text": project.get("description") or project["name"],
"phase": phase["role"],
"phase_order": phase["phase_order"],
"workflow": "research",
}
2026-03-19 19:06:18 +02:00
# knowledge_synthesizer: collect approved researcher outputs into phases_context
if phase["role"] == "knowledge_synthesizer":
all_phases = models.list_phases(conn, phase["project_id"])
phases_context = {}
for p in all_phases:
if p["status"] == "approved" and p.get("task_id"):
row = conn.execute(
"""SELECT output_summary FROM agent_logs
WHERE task_id = ? AND success = 1
ORDER BY created_at DESC LIMIT 1""",
(p["task_id"],),
).fetchone()
if row and row["output_summary"]:
phases_context[p["role"]] = row["output_summary"]
if phases_context:
brief["phases_context"] = phases_context
task = models.create_task(
conn, task_id, phase["project_id"],
title=f"[Research] {ROLE_LABELS.get(phase['role'], phase['role'])}",
assigned_role=phase["role"],
brief=brief,
status="pending",
category=None,
)
updated = models.update_phase(conn, phase_id, task_id=task["id"], status="active")
return updated
def approve_phase(conn: sqlite3.Connection, phase_id: int) -> dict:
"""Approve a phase, activate the next one (or finish workflow).
Returns {phase, next_phase|None}.
"""
phase = models.get_phase(conn, phase_id)
if not phase:
raise ValueError(f"Phase {phase_id} not found")
if phase["status"] != "active":
raise ValueError(f"Phase {phase_id} is not active (current: {phase['status']})")
updated = models.update_phase(conn, phase_id, status="approved")
# Find next pending phase
all_phases = models.list_phases(conn, phase["project_id"])
next_phase = None
for p in all_phases:
if p["phase_order"] > phase["phase_order"] and p["status"] == "pending":
next_phase = p
break
if next_phase:
activated = activate_phase(conn, next_phase["id"])
return {"phase": updated, "next_phase": activated}
return {"phase": updated, "next_phase": None}
def reject_phase(conn: sqlite3.Connection, phase_id: int, reason: str) -> dict:
"""Reject a phase (director rejects the research output entirely)."""
phase = models.get_phase(conn, phase_id)
if not phase:
raise ValueError(f"Phase {phase_id} not found")
if phase["status"] != "active":
raise ValueError(f"Phase {phase_id} is not active (current: {phase['status']})")
return models.update_phase(conn, phase_id, status="rejected")
def revise_phase(conn: sqlite3.Connection, phase_id: int, comment: str) -> dict:
"""Request revision: create a new task for the same role with the comment.
Returns {phase, new_task}.
"""
phase = models.get_phase(conn, phase_id)
if not phase:
raise ValueError(f"Phase {phase_id} not found")
if phase["status"] not in ("active", "revising"):
raise ValueError(
f"Phase {phase_id} cannot be revised (current: {phase['status']})"
)
project = models.get_project(conn, phase["project_id"])
if not project:
raise ValueError(f"Project {phase['project_id']} not found")
new_task_id = models.next_task_id(conn, phase["project_id"], category=None)
brief = {
"text": project.get("description") or project["name"],
"phase": phase["role"],
"phase_order": phase["phase_order"],
"workflow": "research",
"revise_comment": comment,
"revise_count": (phase.get("revise_count") or 0) + 1,
}
new_task = models.create_task(
conn, new_task_id, phase["project_id"],
title=f"[Research Revise] {ROLE_LABELS.get(phase['role'], phase['role'])}",
assigned_role=phase["role"],
brief=brief,
status="pending",
category=None,
)
new_revise_count = (phase.get("revise_count") or 0) + 1
updated = models.update_phase(
conn, phase_id,
status="revising",
task_id=new_task["id"],
revise_count=new_revise_count,
kin: KIN-059 Workflow new_project с выбором команды. При создании нового проекта через GUI или CLI директор описывает проект свободным текстом и выбирает галочками какие этапы research нужны: ☐ Business analyst (бизнес-модель, аудитория, монетизация) ☐ Market researcher (конкуренты, ниша, отзывы, сильные/слабые стороны) ☐ Legal researcher (юрисдикция, лицензии, KYC/AML, GDPR) ☐ Tech researcher (API, ограничения, стоимость, альтернативы) ☐ UX designer (анализ UX конкурентов, user journey, wireframes) ☐ Marketer (стратегия продвижения, SEO, conversion-паттерны) ☐ Architect (blueprint на основе одобренных research'ей) — всегда последний Architect включается автоматически если выбран хотя бы один researcher. Каждый выбранный этап — отдельная задача на review. Директор одобряет, отклоняет, или просит доисследовать (Revise). Следующий этап только после approve предыдущего. GUI: форма 'New Project' с описанием + чекбоксы ролей + кнопка 'Start Research'. CLI: kin new-project 'описание' --roles 'business,market,tech,architect'
2026-03-16 09:30:00 +02:00
revise_comment=comment,
)
return {"phase": updated, "new_task": new_task}