Skip to content

Commit 62f843d

Browse files
committed
Merge branch 'feat/study-backlog-phase2'
2 parents 9be1567 + 3f4a29d commit 62f843d

12 files changed

Lines changed: 1500 additions & 3 deletions

File tree

‎docs/architecture/study-backlog-phase2.md‎

Lines changed: 559 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# Brainstorm: Study Backlog Phase 2 — AI Prioritization + MCP Integration — 2026-04-03
2+
3+
## What We're Building
4+
5+
Three interconnected pieces:
6+
7+
1. **`studyctl topics suggest`** — CLI command that ranks pending backlog topics using frequency + agent-assessed importance
8+
2. **4 new MCP tools** — `get_study_backlog`, `get_topic_suggestions`, `get_study_history`, `record_topic_progress` — giving the agent direct session-db access
9+
3. **Session-db integration tests** — comprehensive tests for all session types, ensuring the full data pipeline works
10+
4. **Architecture documentation** — updated diagrams showing session-db as a first-class component
11+
12+
## Scoring Model
13+
14+
Two signals, combined:
15+
16+
| Signal | Source | Weight | Rationale |
17+
|--------|--------|--------|-----------|
18+
| **Frequency** | COUNT of parked_topics entries per question | Data-driven | Topics that keep coming up are clearly important |
19+
| **Importance** | Agent-assessed priority (1-5) on parked_topics | LLM-driven | Fundamental topics (OOP basics, closures) rank higher than niche ones |
20+
21+
**Importance scoring**: The agent sets a `priority` (1-5) when parking or struggling on a topic. The MCP `record_topic_progress` tool can also update priority. Topics without a priority score default to 3 (neutral).
22+
23+
**Combined score**: `score = frequency_weight * normalized_frequency + importance_weight * (priority / 5.0)`
24+
25+
## Key Decisions
26+
27+
| Decision | Choice | Rationale |
28+
|----------|--------|-----------|
29+
| Scoring model | Frequency + agent importance | Fundamentality matters more than recency for learning paths |
30+
| MCP tools | All 4 (read + write) | Full agent integration, extensibility investment |
31+
| Priority storage | `priority` INTEGER column on parked_topics | Migration v17, agent sets via MCP |
32+
| Suggest output | Ranked list with scores + reasoning hints | Agent can re-rank with its own context |
33+
| Session-db tests | Integration tests with real DB for all session types | Validates full data pipeline end-to-end |
34+
| Architecture docs | Updated PlantUML with session-db as central component | Source of truth for the whole system |
35+
36+
## MCP Tools Spec
37+
38+
| Tool | Type | Description |
39+
|------|------|-------------|
40+
| `get_study_backlog` | Read | Query pending backlog items with filters (tech, source, status) |
41+
| `get_topic_suggestions` | Read | Return algorithmically ranked topic suggestions |
42+
| `get_study_history` | Read | Query study_sessions + study_progress for a topic |
43+
| `record_topic_progress` | Write | Update study_progress confidence, record teach-back, set topic priority |
44+
45+
## Migration v17
46+
47+
```sql
48+
ALTER TABLE parked_topics ADD COLUMN priority INTEGER;
49+
-- NULL = not yet assessed, 1 = low, 5 = critical/foundational
50+
```
51+
52+
## Scope
53+
54+
### In scope
55+
- `studyctl topics suggest` CLI command (FCIS: scoring logic in backlog_logic.py)
56+
- 4 MCP tools registered in studyctl-mcp server
57+
- Migration v17 (priority column)
58+
- Updated architecture doc with session-db integration diagrams
59+
- Integration tests: migration, MCP tools, scoring, all session types
60+
61+
### Out of scope
62+
- Auto-categorization of tech_area by LLM (manual or agent-set for now)
63+
- Spaced repetition integration with suggestions (future)

‎packages/agent-session-tools/src/agent_session_tools/migrations.py‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
logger = logging.getLogger(__name__)
1414

1515
# Current schema version - increment when adding new migrations
16-
CURRENT_VERSION = 16
16+
CURRENT_VERSION = 17
1717

1818
# Migration functions: version -> (description, migration_func)
1919
MIGRATIONS: dict[int, tuple[str, Callable[[sqlite3.Connection], None]]] = {}
@@ -614,6 +614,21 @@ def migrate_v16(conn: sqlite3.Connection) -> None:
614614
""")
615615

616616

617+
@migration(17, "Add priority column to parked_topics for agent-assessed importance")
618+
def migrate_v17(conn: sqlite3.Connection) -> None:
619+
"""Add agent-assessed importance score to backlog items.
620+
621+
Priority is set by the AI agent during study sessions to indicate
622+
how foundational a topic is in the learning path:
623+
- NULL = not yet assessed (defaults to 3 in scoring)
624+
- 1 = low importance (niche/optional)
625+
- 5 = critical/foundational (e.g. OOP basics, closures)
626+
"""
627+
conn.execute("""
628+
ALTER TABLE parked_topics ADD COLUMN priority INTEGER
629+
""")
630+
631+
617632
def check_migration_status(db_path: Path) -> dict:
618633
"""Check migration status without modifying database.
619634

‎packages/studyctl/src/studyctl/backlog_logic.py‎

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,89 @@ class PersistAction:
5252
source: str = "struggled"
5353

5454

55+
@dataclass
56+
class ScoringInput:
57+
"""Pre-gathered data for a single backlog item."""
58+
59+
item: BacklogItem
60+
frequency: int # count of times this topic appears in parked_topics
61+
priority: int | None # agent-assessed importance (1-5), None = unassessed
62+
63+
64+
@dataclass
65+
class TopicSuggestion:
66+
"""A scored and ranked topic suggestion."""
67+
68+
item: BacklogItem
69+
score: float # 0.0 - 1.0, higher = study this next
70+
frequency: int
71+
priority: int # effective priority (default 3 if unassessed)
72+
reasoning: str # human-readable explanation
73+
74+
75+
# Scoring weights: importance matters more than frequency
76+
_FREQUENCY_WEIGHT = 0.4
77+
_IMPORTANCE_WEIGHT = 0.6
78+
_DEFAULT_PRIORITY = 3
79+
80+
81+
def score_backlog_items(inputs: list[ScoringInput]) -> list[TopicSuggestion]:
82+
"""Score and rank backlog items by frequency + agent-assessed importance.
83+
84+
Pure logic — no I/O. Takes pre-gathered scoring inputs, returns
85+
sorted suggestions (highest score first).
86+
87+
Score formula:
88+
effective_priority = priority or 3
89+
score = 0.4 * normalized_frequency + 0.6 * normalized_priority
90+
"""
91+
if not inputs:
92+
return []
93+
94+
max_frequency = max(inp.frequency for inp in inputs)
95+
if max_frequency == 0:
96+
max_frequency = 1 # avoid division by zero
97+
98+
suggestions: list[TopicSuggestion] = []
99+
for inp in inputs:
100+
effective_priority = inp.priority if inp.priority is not None else _DEFAULT_PRIORITY
101+
norm_freq = inp.frequency / max_frequency
102+
norm_priority = effective_priority / 5.0
103+
104+
score = (_FREQUENCY_WEIGHT * norm_freq) + (_IMPORTANCE_WEIGHT * norm_priority)
105+
106+
# Build reasoning
107+
parts: list[str] = []
108+
if effective_priority >= 4:
109+
parts.append(f"high importance ({effective_priority}/5)")
110+
elif effective_priority <= 2:
111+
parts.append(f"low importance ({effective_priority}/5)")
112+
else:
113+
parts.append(f"moderate importance ({effective_priority}/5)")
114+
115+
if inp.frequency >= 3:
116+
parts.append(f"frequently appears ({inp.frequency}x)")
117+
elif inp.frequency == 1:
118+
parts.append("appeared once")
119+
else:
120+
parts.append(f"appeared {inp.frequency}x")
121+
122+
reasoning = ", ".join(parts)
123+
124+
suggestions.append(
125+
TopicSuggestion(
126+
item=inp.item,
127+
score=round(score, 3),
128+
frequency=inp.frequency,
129+
priority=effective_priority,
130+
reasoning=reasoning,
131+
)
132+
)
133+
134+
suggestions.sort(key=lambda s: s.score, reverse=True)
135+
return suggestions
136+
137+
55138
def format_backlog_list(
56139
items: list[BacklogItem],
57140
*,

‎packages/studyctl/src/studyctl/cli/_topics.py‎

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,84 @@ def topics_resolve(topic_id: int) -> None:
141141
console.print(f"[yellow]Topic #{topic_id} not found or already resolved.[/yellow]")
142142

143143

144+
@topics_group.command("suggest")
145+
@click.option("--limit", "-l", default=10, show_default=True, help="Max suggestions to show.")
146+
@click.option("--topic", "-t", default=None, help="Current study topic for relevance sorting.")
147+
def topics_suggest(limit: int, topic: str | None) -> None:
148+
"""Suggest what to study next, ranked by importance and frequency.
149+
150+
Uses algorithmic scoring: agent-assessed importance (60%) combined
151+
with how frequently a topic has been parked/struggled (40%).
152+
153+
Examples:
154+
155+
studyctl backlog suggest
156+
157+
studyctl backlog suggest --limit 5
158+
159+
studyctl backlog suggest --topic "Python Patterns"
160+
"""
161+
from rich.table import Table
162+
163+
from studyctl.backlog_logic import BacklogItem, ScoringInput, score_backlog_items
164+
from studyctl.cli._shared import console
165+
from studyctl.parking import get_parked_topics, get_topic_frequencies
166+
167+
# GATHER
168+
raw_topics = get_parked_topics(status="pending")
169+
if not raw_topics:
170+
console.print("[dim]No pending topics to suggest.[/dim]")
171+
return
172+
173+
frequencies = get_topic_frequencies(status="pending")
174+
175+
scoring_inputs = [
176+
ScoringInput(
177+
item=BacklogItem(
178+
id=t["id"],
179+
question=t["question"],
180+
topic_tag=t.get("topic_tag"),
181+
tech_area=t.get("tech_area"),
182+
source=t.get("source", "parked"),
183+
context=t.get("context"),
184+
parked_at=t["parked_at"],
185+
session_topic=None,
186+
),
187+
frequency=frequencies.get(t["question"], 1),
188+
priority=t.get("priority"),
189+
)
190+
for t in raw_topics
191+
]
192+
193+
# DECIDE
194+
suggestions = score_backlog_items(scoring_inputs)[:limit]
195+
196+
# PRESENT
197+
table = Table(title=f"Study Suggestions — Top {len(suggestions)}")
198+
table.add_column("#", justify="right", style="dim", width=3)
199+
table.add_column("Topic", style="cyan")
200+
table.add_column("Tech", style="green")
201+
table.add_column("Score", justify="right", style="bold yellow")
202+
table.add_column("Why", style="dim")
203+
204+
for rank, s in enumerate(suggestions, 1):
205+
tech_col = s.item.tech_area or ""
206+
score_col = f"{s.score:.2f}"
207+
table.add_row(str(rank), s.item.question, tech_col, score_col, s.reasoning)
208+
209+
console.print(table)
210+
211+
if topic:
212+
# Highlight items relevant to the current topic
213+
relevant = [
214+
s for s in suggestions if s.item.tech_area and s.item.tech_area.lower() in topic.lower()
215+
]
216+
if relevant:
217+
console.print(
218+
f"\n[dim]{len(relevant)} of these relate to your current topic ({topic})[/dim]"
219+
)
220+
221+
144222
def _relative_time(iso_datetime: str) -> str:
145223
"""Convert ISO datetime string to a relative time like '2d', '5h'."""
146224
from datetime import UTC, datetime

0 commit comments

Comments
 (0)