darkplex-core/cortex/auto_handoff.py
Claudia 43d033e242 feat: initial cortex package — 8 intelligence modules, CLI, Docker
Modules: triage, health_scanner, feedback_loop, memory_hygiene,
         roadmap, validate_output, enhanced_search, auto_handoff
         + composite_scorer, intent_classifier

CLI: 'cortex <module> <command>' unified entry point
Tests: 157/169 passing (12 assertion mismatches from rename)
Docker: python:3.11-slim based
2026-02-09 11:18:20 +01:00

246 lines
7.4 KiB
Python
Executable file

#!/usr/bin/env python3
"""
Auto-Handoff Generator — Extract structured session handoffs from daily notes
or session transcripts.
Two modes:
1. LLM-based extraction (via OpenAI API) — comprehensive
2. Regex-based fallback — extracts TODO/DECISION/BLOCKED/QUESTION markers
Output: Structured markdown handoff document.
"""
import json
import os
import re
import sys
from datetime import datetime
from pathlib import Path
from typing import Optional
CONFIG_PATH = Path(__file__).parent / "config.json"
def load_config(path: Optional[Path] = None) -> dict:
"""Load handoff config."""
p = path or CONFIG_PATH
if p.exists():
with open(p) as f:
return json.load(f).get("auto_handoff", {})
return {}
def extract_markers(content: str, config: Optional[dict] = None) -> dict:
"""Regex-based extraction of structured markers from content.
Looks for TODO:, DECISION:, BLOCKED:, QUESTION: and similar markers.
Returns dict with lists of decisions, todos, blocked items, and questions.
"""
cfg = config or load_config()
markers_cfg = cfg.get("markers", {})
results = {
"decisions": [],
"todos": [],
"blocked": [],
"questions": [],
}
category_map = {
"decision": "decisions",
"todo": "todos",
"blocked": "blocked",
"question": "questions",
}
lines = content.split("\n")
for line in lines:
stripped = line.strip()
if not stripped:
continue
for marker_type, marker_list in markers_cfg.items():
for marker in marker_list:
if marker in stripped:
# Extract the text after the marker
idx = stripped.index(marker) + len(marker)
text = stripped[idx:].strip().lstrip(":").strip()
if text:
category = category_map.get(marker_type, marker_type)
if category in results:
results[category].append(text)
break
# Also extract checkbox items as todos
checkbox_re = re.compile(r'^[-*]\s*\[\s*\]\s*(.+)')
done_re = re.compile(r'^[-*]\s*\[x\]\s*(.+)', re.IGNORECASE)
for line in lines:
m = checkbox_re.match(line.strip())
if m and m.group(1) not in results["todos"]:
results["todos"].append(m.group(1))
# Extract headings with key context
results["key_context"] = []
heading_re = re.compile(r'^#{1,3}\s+(.+)')
for line in lines:
m = heading_re.match(line.strip())
if m:
results["key_context"].append(m.group(1))
return results
def format_handoff(extracted: dict, title: str = "Session Handoff",
source: str = "") -> str:
"""Format extracted data into a structured markdown handoff document."""
now = datetime.now().strftime("%Y-%m-%d %H:%M")
parts = [f"# {title}", f"*Generated: {now}*"]
if source:
parts.append(f"*Source: {source}*")
parts.append("")
if extracted.get("decisions"):
parts.append("## ✅ Decisions Made")
for d in extracted["decisions"]:
parts.append(f"- {d}")
parts.append("")
if extracted.get("todos"):
parts.append("## 📋 Next Steps / TODOs")
for t in extracted["todos"]:
parts.append(f"- [ ] {t}")
parts.append("")
if extracted.get("blocked"):
parts.append("## ⚠️ Blocked / Waiting")
for b in extracted["blocked"]:
parts.append(f"- {b}")
parts.append("")
if extracted.get("questions"):
parts.append("## ❓ Open Questions")
for q in extracted["questions"]:
parts.append(f"- {q}")
parts.append("")
if extracted.get("key_context"):
parts.append("## 📌 Key Context")
for c in extracted["key_context"]:
parts.append(f"- {c}")
parts.append("")
if extracted.get("summary"):
parts.append("## 📝 Summary")
parts.append(extracted["summary"])
parts.append("")
return "\n".join(parts)
def generate_handoff_llm(content: str, config: Optional[dict] = None) -> dict:
"""Use OpenAI API to extract structured handoff data from content.
Requires OPENAI_API_KEY environment variable.
Falls back to regex extraction if API unavailable.
"""
api_key = os.environ.get("OPENAI_API_KEY")
if not api_key:
return extract_markers(content, config)
cfg = config or load_config()
model = cfg.get("llm_model", "gpt-4o-mini")
max_tokens = cfg.get("llm_max_tokens", 1500)
try:
import urllib.request
prompt = f"""Extract structured information from this session log/daily note.
Return JSON with these fields:
- "decisions": list of decisions made
- "todos": list of action items / next steps
- "blocked": list of blocked items or things waiting on someone
- "questions": list of open questions
- "summary": brief 2-3 sentence summary of the session
- "key_context": list of important context points for the next session
Content:
{content[:8000]}"""
body = json.dumps({
"model": model,
"messages": [{"role": "user", "content": prompt}],
"max_tokens": max_tokens,
"response_format": {"type": "json_object"},
}).encode()
req = urllib.request.Request(
"https://api.openai.com/v1/chat/completions",
data=body,
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
},
)
with urllib.request.urlopen(req, timeout=30) as resp:
data = json.loads(resp.read())
content_str = data["choices"][0]["message"]["content"]
return json.loads(content_str)
except Exception as e:
print(f"LLM extraction failed ({e}), falling back to regex", file=sys.stderr)
return extract_markers(content, config)
def generate_handoff(content: str, source: str = "", use_llm: bool = False,
config: Optional[dict] = None) -> str:
"""Generate a complete handoff document from content.
Args:
content: Session transcript or daily note content.
source: Source file path for attribution.
use_llm: Whether to use LLM extraction (requires OPENAI_API_KEY).
config: Optional config dict.
Returns:
Formatted markdown handoff document.
"""
if use_llm:
extracted = generate_handoff_llm(content, config)
else:
extracted = extract_markers(content, config)
return format_handoff(extracted, source=source)
def main():
_run()
def _run():
import argparse
parser = argparse.ArgumentParser(description="Generate session handoff document")
parser.add_argument("file", nargs="?", help="Input file (daily note or transcript)")
parser.add_argument("--llm", action="store_true", help="Use LLM for extraction")
parser.add_argument("--output", "-o", help="Output file (default: stdout)")
args = parser.parse_args()
if args.file:
content = Path(args.file).read_text()
source = args.file
else:
content = sys.stdin.read()
source = "stdin"
result = generate_handoff(content, source=source, use_llm=args.llm)
if args.output:
Path(args.output).write_text(result)
print(f"Handoff written to {args.output}")
else:
print(result)
if __name__ == "__main__":
main()