kin: KIN-135-backend_dev
This commit is contained in:
parent
4c01e0e4ee
commit
24be66d16c
9 changed files with 436 additions and 6 deletions
|
|
@ -70,6 +70,17 @@ def build_context(
|
|||
ctx["available_specialists"] = []
|
||||
ctx["routes"] = {}
|
||||
ctx["departments"] = {}
|
||||
# KIN-135: return history for escalation routing
|
||||
try:
|
||||
return_count = (task or {}).get("return_count") or 0
|
||||
ctx["return_count"] = return_count
|
||||
if return_count > 0:
|
||||
ctx["return_history"] = models.get_task_returns(conn, task_id, limit=5)
|
||||
else:
|
||||
ctx["return_history"] = []
|
||||
except Exception:
|
||||
ctx["return_count"] = 0
|
||||
ctx["return_history"] = []
|
||||
|
||||
elif role == "architect":
|
||||
ctx["modules"] = models.get_modules(conn, project_id)
|
||||
|
|
@ -151,6 +162,17 @@ def build_context(
|
|||
except Exception:
|
||||
pass
|
||||
|
||||
elif role == "return_analyst":
|
||||
# KIN-135: return analyst needs full return history and decisions
|
||||
ctx["decisions"] = models.get_decisions(conn, project_id)
|
||||
try:
|
||||
return_count = (task or {}).get("return_count") or 0
|
||||
ctx["return_count"] = return_count
|
||||
ctx["return_history"] = models.get_task_returns(conn, task_id, limit=20)
|
||||
except Exception:
|
||||
ctx["return_count"] = 0
|
||||
ctx["return_history"] = []
|
||||
|
||||
else:
|
||||
# Unknown role — give decisions as fallback
|
||||
ctx["decisions"] = models.get_decisions(conn, project_id, limit=20)
|
||||
|
|
@ -297,6 +319,20 @@ def format_prompt(context: dict, role: str, prompt_template: str | None = None)
|
|||
sections.append(f"- {t['id']}: {t['title']} [{t['status']}]")
|
||||
sections.append("")
|
||||
|
||||
# Return history (PM) — KIN-135
|
||||
return_count = context.get("return_count", 0)
|
||||
return_history = context.get("return_history")
|
||||
if return_count and return_count > 0:
|
||||
sections.append(f"## Return History (return_count={return_count}):")
|
||||
if return_history:
|
||||
for r in return_history:
|
||||
reason_text = f" — {r['reason_text']}" if r.get("reason_text") else ""
|
||||
sections.append(
|
||||
f"- #{r['return_number']} [{r['reason_category']}]{reason_text} "
|
||||
f"(returned_by={r.get('returned_by', 'system')}, at={r.get('returned_at', '')})"
|
||||
)
|
||||
sections.append("")
|
||||
|
||||
# Available specialists (PM)
|
||||
specialists = context.get("available_specialists")
|
||||
if specialists:
|
||||
|
|
|
|||
49
core/db.py
49
core/db.py
|
|
@ -72,6 +72,7 @@ CREATE TABLE IF NOT EXISTS tasks (
|
|||
telegram_sent BOOLEAN DEFAULT 0,
|
||||
acceptance_criteria TEXT,
|
||||
smoke_test_result JSON DEFAULT NULL,
|
||||
return_count INTEGER DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
|
@ -151,6 +152,7 @@ CREATE TABLE IF NOT EXISTS pipelines (
|
|||
parent_pipeline_id INTEGER REFERENCES pipelines(id),
|
||||
department TEXT,
|
||||
pid INTEGER,
|
||||
pipeline_type TEXT DEFAULT 'standard',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
completed_at DATETIME
|
||||
);
|
||||
|
|
@ -326,6 +328,20 @@ CREATE TABLE IF NOT EXISTS pipeline_log (
|
|||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_pipeline_log_pipeline_id ON pipeline_log(pipeline_id, id);
|
||||
|
||||
-- История возвратов задачи к PM (KIN-135)
|
||||
CREATE TABLE IF NOT EXISTS task_returns (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
||||
return_number INTEGER NOT NULL,
|
||||
reason_category TEXT NOT NULL,
|
||||
reason_text TEXT,
|
||||
returned_by TEXT DEFAULT 'system',
|
||||
pipeline_id INTEGER REFERENCES pipelines(id),
|
||||
returned_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_task_returns_task_id ON task_returns(task_id);
|
||||
"""
|
||||
|
||||
|
||||
|
|
@ -800,6 +816,39 @@ def _migrate(conn: sqlite3.Connection):
|
|||
conn.execute("ALTER TABLE tasks ADD COLUMN smoke_test_result JSON DEFAULT NULL")
|
||||
conn.commit()
|
||||
|
||||
# KIN-135: Add return_count to tasks — counts full returns to PM
|
||||
task_cols_final3 = {r[1] for r in conn.execute("PRAGMA table_info(tasks)").fetchall()}
|
||||
if "return_count" not in task_cols_final3:
|
||||
conn.execute("ALTER TABLE tasks ADD COLUMN return_count INTEGER DEFAULT 0")
|
||||
conn.commit()
|
||||
|
||||
# KIN-135: Add pipeline_type to pipelines — distinguishes escalation pipelines
|
||||
if "pipelines" in existing_tables:
|
||||
pipeline_cols2 = {r[1] for r in conn.execute("PRAGMA table_info(pipelines)").fetchall()}
|
||||
if "pipeline_type" not in pipeline_cols2:
|
||||
conn.execute("ALTER TABLE pipelines ADD COLUMN pipeline_type TEXT DEFAULT 'standard'")
|
||||
conn.commit()
|
||||
|
||||
# KIN-135: Create task_returns table — return history per task
|
||||
existing_tables2 = {r[0] for r in conn.execute(
|
||||
"SELECT name FROM sqlite_master WHERE type='table'"
|
||||
).fetchall()}
|
||||
if "task_returns" not in existing_tables2:
|
||||
conn.executescript("""
|
||||
CREATE TABLE IF NOT EXISTS task_returns (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
||||
return_number INTEGER NOT NULL,
|
||||
reason_category TEXT NOT NULL,
|
||||
reason_text TEXT,
|
||||
returned_by TEXT DEFAULT 'system',
|
||||
pipeline_id INTEGER REFERENCES pipelines(id),
|
||||
returned_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_task_returns_task_id ON task_returns(task_id);
|
||||
""")
|
||||
conn.commit()
|
||||
|
||||
|
||||
def _seed_default_hooks(conn: sqlite3.Connection):
|
||||
"""Seed default hooks for the kin project (idempotent).
|
||||
|
|
|
|||
104
core/models.py
104
core/models.py
|
|
@ -23,6 +23,16 @@ TASK_CATEGORIES = [
|
|||
"ARCH", "TEST", "PERF", "DOCS", "FIX", "OBS",
|
||||
]
|
||||
|
||||
# Valid categories for task returns (KIN-135)
|
||||
RETURN_CATEGORIES = [
|
||||
"requirements_unclear",
|
||||
"scope_too_large",
|
||||
"technical_blocker",
|
||||
"missing_context",
|
||||
"recurring_quality_fail",
|
||||
"conflicting_requirements",
|
||||
]
|
||||
|
||||
|
||||
def validate_completion_mode(value: str) -> str:
|
||||
"""Validate completion mode from LLM output. Falls back to 'review' if invalid."""
|
||||
|
|
@ -1301,3 +1311,97 @@ def get_chat_messages(
|
|||
query += " ORDER BY created_at ASC, id ASC LIMIT ?"
|
||||
params.append(limit)
|
||||
return _rows_to_list(conn.execute(query, params).fetchall())
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Task returns — escalation tracking (KIN-135)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def record_task_return(
|
||||
conn: sqlite3.Connection,
|
||||
task_id: str,
|
||||
reason_category: str,
|
||||
reason_text: str | None = None,
|
||||
returned_by: str = "system",
|
||||
pipeline_id: int | None = None,
|
||||
) -> dict:
|
||||
"""Record a task return to PM and increment return_count.
|
||||
|
||||
reason_category must be one of RETURN_CATEGORIES; defaults to 'missing_context'
|
||||
if an invalid value is supplied (fail-open).
|
||||
Returns the inserted task_returns row as dict.
|
||||
"""
|
||||
if reason_category not in RETURN_CATEGORIES:
|
||||
reason_category = "missing_context"
|
||||
|
||||
# Determine the next return_number for this task
|
||||
row = conn.execute(
|
||||
"SELECT COALESCE(MAX(return_number), 0) FROM task_returns WHERE task_id = ?",
|
||||
(task_id,),
|
||||
).fetchone()
|
||||
next_number = (row[0] if row else 0) + 1
|
||||
|
||||
conn.execute(
|
||||
"""INSERT INTO task_returns
|
||||
(task_id, return_number, reason_category, reason_text, returned_by, pipeline_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?)""",
|
||||
(task_id, next_number, reason_category, reason_text, returned_by, pipeline_id),
|
||||
)
|
||||
conn.execute(
|
||||
"UPDATE tasks SET return_count = return_count + 1 WHERE id = ?",
|
||||
(task_id,),
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
inserted = conn.execute(
|
||||
"SELECT * FROM task_returns WHERE task_id = ? ORDER BY id DESC LIMIT 1",
|
||||
(task_id,),
|
||||
).fetchone()
|
||||
return dict(inserted) if inserted else {}
|
||||
|
||||
|
||||
def get_task_returns(
|
||||
conn: sqlite3.Connection,
|
||||
task_id: str,
|
||||
limit: int = 20,
|
||||
) -> list[dict]:
|
||||
"""Return task return history ordered by return_number ASC."""
|
||||
rows = conn.execute(
|
||||
"SELECT * FROM task_returns WHERE task_id = ? ORDER BY return_number ASC LIMIT ?",
|
||||
(task_id, limit),
|
||||
).fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
|
||||
def check_return_pattern(
|
||||
conn: sqlite3.Connection,
|
||||
task_id: str,
|
||||
) -> dict:
|
||||
"""Analyse return history for a recurring pattern.
|
||||
|
||||
Returns:
|
||||
{
|
||||
"pattern_detected": bool, # True if dominant_category appears >= 2 times
|
||||
"dominant_category": str | None,
|
||||
"occurrences": int,
|
||||
}
|
||||
"""
|
||||
from collections import Counter
|
||||
|
||||
rows = conn.execute(
|
||||
"SELECT reason_category FROM task_returns WHERE task_id = ?",
|
||||
(task_id,),
|
||||
).fetchall()
|
||||
|
||||
if not rows:
|
||||
return {"pattern_detected": False, "dominant_category": None, "occurrences": 0}
|
||||
|
||||
counts = Counter(r[0] for r in rows)
|
||||
dominant_category, occurrences = counts.most_common(1)[0]
|
||||
pattern_detected = occurrences >= 2
|
||||
|
||||
return {
|
||||
"pattern_detected": pattern_detected,
|
||||
"dominant_category": dominant_category,
|
||||
"occurrences": occurrences,
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue