Files
oO/services/api/src/config.ts
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

41 lines
1.5 KiB
TypeScript

import { config as dotenvConfig } from 'dotenv';
// Load .env.local first (takes precedence), then .env as fallback
dotenvConfig({ path: '../../.env.local', override: false });
dotenvConfig({ path: '../../.env', override: false });
function require(name: string): string {
const val = process.env[name];
if (!val) throw new Error(`Missing required env var: ${name}`);
return val;
}
function optional(name: string, fallback: string): string {
return process.env[name] ?? fallback;
}
export const config = {
PORT: parseInt(optional('PORT', '3001'), 10),
NODE_ENV: optional('NODE_ENV', 'development'),
DATABASE_PATH: optional('DATABASE_PATH', './data/oo.db'),
SESSION_SECRET: require('SESSION_SECRET'),
GOOGLE_CLIENT_ID: require('GOOGLE_CLIENT_ID'),
GOOGLE_CLIENT_SECRET: require('GOOGLE_CLIENT_SECRET'),
TODOIST_CLIENT_ID: require('TODOIST_CLIENT_ID'),
TODOIST_CLIENT_SECRET: require('TODOIST_CLIENT_SECRET'),
/** Absolute base URL of this API, e.g. http://localhost:3001 */
API_BASE_URL: optional('API_BASE_URL', 'http://localhost:3001'),
/** Absolute base URL of the web app, e.g. http://localhost:3000 */
WEB_BASE_URL: optional('WEB_BASE_URL', 'http://localhost:3000'),
ML_SERVING_URL: optional('ML_SERVING_URL', 'http://localhost:8000'),
LITELLM_URL: optional('LITELLM_URL', 'http://localhost:4000'),
VAPID_PUBLIC_KEY: optional('VAPID_PUBLIC_KEY', ''),
VAPID_PRIVATE_KEY: optional('VAPID_PRIVATE_KEY', ''),
VAPID_SUBJECT: optional('VAPID_SUBJECT', 'mailto:admin@localhost'),
};