Files
oO/ml/features/context.py
alvis ffdf70733f feat: M2 AI tips — LiteLLM gateway, context assembler, end-to-end generation pipeline
Issues closed: #86, #87, #88, #89, #90, #91, #79, #80, #82

infra:
- docker-compose `ai` profile: Ollama + LiteLLM services
- infra/litellm/litellm_config.yaml: tip-generator / embedder / judge aliases
- .env.example: LITELLM_URL, LITELLM_MASTER_KEY, OLLAMA_URL

ml/serving:
- POST /generate: calls LiteLLM tip-generator alias, returns TipCandidate[]
- JSON retry loop (2 retries with correction prompt on malformed response)
- _parse_llm_json strips markdown fences

ml/features:
- context.py: build_context() assembles user signals → PromptContext
  (sorts overdue/high-priority tasks first for LLM prompt quality)

shared-types:
- TipKind, TipSource, TipCandidate types
- Tip gains kind + rationale fields

services/api:
- recommender: 3-stage pipeline (assemble → score → serve)
  Stage 1: Todoist tasks + LLM candidates fetched in parallel
  Stage 2: egreedy bandit scores merged candidate pool
  Stage 3: serve + log with prompt_version, llm_model, tip_kind
- tip_scores: prompt_version, llm_model, tip_kind columns + migrations
- config: LITELLM_URL added
- integrations: surface token_status in /integrations response

tests:
- ml/serving/tests/test_generate.py: 13 tests (retry, 502/503, fence variants)
- ml/features/test_context.py: 9 tests (sorting, edge cases)
- services/api recommender.unit.test.ts: 16 pure-function tests (inferReward, dueAgeDays)
- services/api recommender.test.ts: 4 integration tests (tip_scores columns, LLM fallback)
- shared-types: TipCandidate, rationale, full TipFeedback action set

docs:
- ADR-0008: LiteLLM AI gateway decision
- overview.md: M2 pipeline description updated
- ml/README.md: serving + features roles updated

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 14:09:02 +00:00

64 lines
1.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Context assembler — converts raw user signals into a PromptContext for LLM tip generation.
Usage:
from ml.features.context import build_context
ctx = build_context(tasks, hour_of_day=9, day_of_week=2)
"""
from __future__ import annotations
from dataclasses import dataclass, field
@dataclass
class TaskSignal:
id: str
content: str
priority: int = 1 # 14 (Todoist scale)
is_overdue: bool = False
task_age_days: float = 0.0
due_date: str | None = None
@dataclass
class PromptContext:
tasks: list[dict] = field(default_factory=list)
hour_of_day: int = 12
day_of_week: int = 0
extra: dict = field(default_factory=dict)
def build_context(
tasks: list[TaskSignal],
hour_of_day: int = 12,
day_of_week: int = 0,
extra: dict | None = None,
) -> PromptContext:
"""
Assemble user signals into a PromptContext.
Signals are sorted so overdue + high-priority tasks appear first,
giving the LLM the most actionable context at the top of the prompt.
"""
sorted_tasks = sorted(
tasks,
key=lambda t: (not t.is_overdue, -t.priority, -t.task_age_days),
)
task_dicts = [
{
"id": t.id,
"content": t.content,
"priority": t.priority,
"is_overdue": t.is_overdue,
"task_age_days": round(t.task_age_days, 1),
"due_date": t.due_date,
}
for t in sorted_tasks
]
return PromptContext(
tasks=task_dicts,
hour_of_day=hour_of_day,
day_of_week=day_of_week,
extra=extra or {},
)