Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 6 additions & 7 deletions app/api/ai/briefing/route.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { NextRequest, NextResponse } from 'next/server';
import { createServer } from '@/lib/supabase/server';
import { requireUserContext } from '@/lib/supabase/auth';
import Anthropic from '@anthropic-ai/sdk';

const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
import { resolveProvider } from '@/lib/ai/provider';

export async function POST(req: NextRequest) {
try {
Expand Down Expand Up @@ -146,14 +144,15 @@ Rules:
- For weekly briefing, populate weeklyHighlights and weeklyConflicts
- If data is sparse, be honest but still encouraging`;

const response = await anthropic.messages.create({
model: process.env.AI_MODEL ?? 'claude-haiku-4-5-20251001',
max_tokens: 2000,
const provider = await resolveProvider();
const completion = await provider.complete({
system: systemPrompt,
messages: [{ role: 'user', content: `Generate ${type} briefing for ${firstName}.\n\nData:\n${context}` }],
tools: [],
maxTokens: 2000,
});

const text = response.content[0].type === 'text' ? response.content[0].text : '{}';
const text = completion.text || '{}';

let briefing: Record<string, unknown>;
try {
Expand Down
17 changes: 13 additions & 4 deletions app/api/ai/flyer/route.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { NextRequest, NextResponse } from 'next/server';
import Anthropic from '@anthropic-ai/sdk';
import { createServer } from '@/lib/supabase/server';
import { createServer, createServiceClient } from '@/lib/supabase/server';
import { requireUserContext } from '@/lib/supabase/auth';
import { rateLimit, clientIp } from '@/lib/server/rate-limit';
import { getAIConfig } from '@/lib/ai/settings';

export const runtime = 'nodejs';

const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });

const IMAGE_TYPES = ['image/jpeg', 'image/png', 'image/webp', 'image/gif'];

type ProposedEvent = {
Expand Down Expand Up @@ -99,8 +98,18 @@ Rules:
? { type: 'document' as const, source: { type: 'base64' as const, media_type: 'application/pdf' as const, data } }
: { type: 'image' as const, source: { type: 'base64' as const, media_type: mediaType as 'image/jpeg' | 'image/png' | 'image/webp' | 'image/gif', data } };

// Flyer parsing needs PDF/vision input, which is Anthropic-specific — use the
// admin-configured Anthropic key/model (Admin → AI Engine), with env fallback.
const aiConfig = await getAIConfig(createServiceClient());
const apiKey = aiConfig.anthropicKey ?? process.env.ANTHROPIC_API_KEY;
if (!apiKey) {
return NextResponse.json({ error: 'Flyer scanning needs an Anthropic API key. Add one in Admin → AI Engine.' }, { status: 503 });
}
const model = aiConfig.provider === 'anthropic' && aiConfig.model ? aiConfig.model : 'claude-sonnet-4-6';
const anthropic = new Anthropic({ apiKey });

const response = await anthropic.messages.create({
model: process.env.AI_MODEL ?? 'claude-sonnet-4-6',
model,
max_tokens: 1500,
messages: [{ role: 'user', content: [filePart, { type: 'text', text: prompt }] }],
});
Expand Down
13 changes: 6 additions & 7 deletions app/api/ai/weekly-briefing/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@ import { createServer } from '@/lib/supabase/server';
import { requireUserContext } from '@/lib/supabase/auth';
import { planLevel } from '@/lib/constants/plans';
import { weekWindow, weekRangeLabel, choreCompletionRate, bucketByDay, dayLoad } from '@/lib/ai/weekly';
import Anthropic from '@anthropic-ai/sdk';

const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
import { resolveProvider } from '@/lib/ai/provider';

/**
* Plus-tier Weekly AI Briefing. Distinct from the daily briefing: it reads a
Expand Down Expand Up @@ -166,14 +164,15 @@ Rules:
- Score categories 0-100 honestly from the data; be encouraging if data is sparse but never invent events.
- Emojis: 🏥 medical, ⚽ sports, 📚 school, ✈️ travel, 🍽️ dinner, 💼 work, 🎂 birthday.`;

const response = await anthropic.messages.create({
model: process.env.AI_MODEL ?? 'claude-haiku-4-5-20251001',
max_tokens: 2600,
const provider = await resolveProvider();
const completion = await provider.complete({
system: systemPrompt,
messages: [{ role: 'user', content: `Generate the weekly briefing for ${firstName}.\n\nData:\n${context}` }],
tools: [],
maxTokens: 2600,
});

const text = response.content[0]?.type === 'text' ? response.content[0].text : '{}';
const text = completion.text || '{}';

let briefing: Record<string, unknown>;
try {
Expand Down
18 changes: 11 additions & 7 deletions docs/AGENT_HANDOFF.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Agent Handoff — Bubaly / FamilyOS

Living context doc so another agent can continue without re-deriving everything.
Last updated after PR #81. Keep this updated as you ship.
Last updated after PR #84. Keep this updated as you ship.

## Product & stack
- **Bubaly / FamilyOS** — a family operating system. Next.js 15 App Router + TS +
Expand Down Expand Up @@ -122,6 +122,7 @@ npx vitest run tests/<your>.test.ts # full suite currently 363 passing
- #79 Configurable AI engine (Claude/Anthropic OR ChatGPT/OpenAI) + admin UI for keys
- #80 Domain canonicalization: `theagoras.com` → `www.bubaly.com` (next.config redirect)
- #81 Removed "Loved by N families" social-proof badge from the marketing hero
- #82 handoff regen · #83 support@bubaly.com everywhere · #84 finished AI-engine wiring (briefing/weekly/flyer)

Latest migration applied to prod: **0048**. Next migration number: **0049**.

Expand All @@ -139,19 +140,22 @@ Latest migration applied to prod: **0048**. Next migration number: **0049**.
- **Wired through:** all provider-based AI routes use `await resolveProvider()` (briefings
via provider, conflict, home AI, marketing AI, social, accident, import) AND the main
assistant `app/api/ai/chat/route.ts`.
- **Still on the Anthropic SDK directly** (follow-up): `app/api/ai/briefing/route.ts`,
`app/api/ai/weekly-briefing/route.ts`, `app/api/ai/flyer/route.ts`
(verify: `grep -rln "@anthropic-ai/sdk" app lib`). Migrate the same way; `flyer` uses
image input which the generic `complete()` interface doesn't model yet (needs extension).
- `complete()` takes an optional `maxTokens` (default 1024) — set it for long JSON outputs.
- **AI wiring is now complete (#84):** `briefing` + `weekly-briefing` go through
`resolveProvider()`. `flyer` stays on the Anthropic SDK on purpose (PDF/vision input is
Anthropic-specific) but reads the admin-configured Anthropic key/model via `getAIConfig`
and returns 503 with a clear message if no Anthropic key is set. If you switch the engine
to OpenAI, flyer still needs an Anthropic key (or build an OpenAI-vision path; note: no PDF).
- To use ChatGPT: `/admin/ai` → pick **ChatGPT (OpenAI)**, choose a model (gpt-4o…),
paste the OpenAI key, Save. Env fallbacks: `OPENAI_API_KEY`, `AI_PROVIDER=openai`, `AI_MODEL`.
(The OpenAI account/key must have active billing or calls 401/429.)

## Backlog (prioritized, each a clean PR)
1. Remaining marketing pillars: **A/B testing**, **Lead scoring**, lifecycle journeys.
2. Finish AI-engine wiring: migrate `briefing` / `weekly-briefing` / `flyer` to `resolveProvider()`.
3. Broader UX brief (Phases 3/4/5/9/11): mobile-first polish, theme-token audit,
2. Broader UX brief (Phases 3/4/5/9/11): mobile-first polish, theme-token audit,
Family Command Center home, AI-native touches, performance.
- (DONE #84) Finish AI-engine wiring: briefing/weekly-briefing → resolveProvider; flyer
reads admin Anthropic key.

## How to continue (quick start for the next agent)
1. Read this whole file. Recreate `/tmp/sbq.mjs` if missing (see migration section);
Expand Down
26 changes: 13 additions & 13 deletions lib/ai/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,24 +19,26 @@ export type AICompletion = {
toolCalls: { name: string; args: Record<string, unknown> }[];
};

export type AICompleteInput = {
system: string;
messages: AIMessage[];
tools: AITool[];
/** Max output tokens. Defaults to 1024. */
maxTokens?: number;
};

export interface AIProvider {
readonly id: string;
readonly model: string;
complete(input: {
system: string;
messages: AIMessage[];
tools: AITool[];
}): Promise<AICompletion>;
complete(input: AICompleteInput): Promise<AICompletion>;
}

// --- Anthropic implementation ---
export class AnthropicProvider implements AIProvider {
id = 'anthropic';
constructor(public model = 'claude-sonnet-4-6', private apiKey = process.env.ANTHROPIC_API_KEY!) {}

async complete({ system, messages, tools }: {
system: string; messages: AIMessage[]; tools: AITool[];
}): Promise<AICompletion> {
async complete({ system, messages, tools, maxTokens = 1024 }: AICompleteInput): Promise<AICompletion> {
const res = await fetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers: {
Expand All @@ -46,7 +48,7 @@ export class AnthropicProvider implements AIProvider {
},
body: JSON.stringify({
model: this.model,
max_tokens: 1024,
max_tokens: maxTokens,
system,
tools: tools.map((t) => ({ name: t.name, description: t.description, input_schema: t.input_schema })),
messages: messages
Expand All @@ -72,13 +74,11 @@ export class OpenAIProvider implements AIProvider {
id = 'openai';
constructor(public model = 'gpt-4o', private apiKey = process.env.OPENAI_API_KEY ?? '') {}

async complete({ system, messages, tools }: {
system: string; messages: AIMessage[]; tools: AITool[];
}): Promise<AICompletion> {
async complete({ system, messages, tools, maxTokens = 1024 }: AICompleteInput): Promise<AICompletion> {
if (!this.apiKey) throw new Error('OpenAI API key is not configured');
const body: Record<string, unknown> = {
model: this.model,
max_tokens: 1024,
max_tokens: maxTokens,
messages: [
...(system ? [{ role: 'system', content: system }] : []),
...messages
Expand Down
Loading