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
51 changes: 31 additions & 20 deletions .github/_auto_pr_body.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,27 @@
# auto: add multimodal image input via Gemini 2.0 Flash
# auto: fix markdown prose styling for non-recipe chat responses

## Why

265 traces show zero image-based queries, yet "fridge photo → recipe" and "dish recreation" are natural, high-value flows. The `GEMINI_API_KEY` is already wired into CI (confirmed via `GEMINI_API_KEY` env var). Adding image input behind a flag lets us ship the capability safely — flag stays off in production until validated, and existing text-only behavior is completely unchanged when the flag is off (image field is silently ignored).
Two real user traces confirm that numbered lists and bullet points are invisible in the chat UI:
- Trace `0ee0a635e5b25b7abe154d9e730c90ab`: User said "I think the markdown is not rendering the numbers" after receiving a numbered dinner-options list.
- Trace `dafdb4a5e3bbb0d1a6e12f2466ae1315`: User asked the agent to format options "like 1, 2, 3" — implying current list rendering was broken.

Note: The operator focus hint specified Gemini 1.5 Flash, which has been retired. The implementation uses **Gemini 2.0 Flash** (`gemini-2.0-flash`), the direct successor at the same cost/speed point.
Root cause: `@tailwindcss/typography` is not installed and not in `tailwind.config.ts` plugins. Tailwind's preflight CSS resets `<ol>/<ul>` defaults — it strips `list-style-type` and `padding-left` from all list elements. The `prose-invert` class used throughout the chat components has **no effect** without the typography plugin, so every numbered list and bullet the agent produces renders as flat, unstyled text with no visible markers.

This affects every non-recipe response: numbered dinner options, clarifying-question lists, substitution bullet points, multi-turn follow-ups.

## What

- `api/main.py`: Added optional `image` field (base64 data URL, HTTP(S) URL, or raw base64) to `ChatRequest`. When `auto_multimodal_images` is ON and `image` is non-null, the `/chat` endpoint calls `_call_gemini_vision()` instead of the OpenAI agent. Added `multimodal_images` key to `/flags` response. Supports data URLs, HTTP image URLs (auto-fetched and base64-encoded), and raw base64.
- `web/components/chat.tsx`: Added `ImagePlus` button in the Premium UI input row (shown only when `multimodal_images` flag is on). Image is selected via hidden file input, previewed as a thumbnail strip above the text field, and sent as a base64 data URL in the request body. User bubbles render the attached image above their text. The send button is enabled when either text OR an image is present.
- `tests/test_multimodal_image.py`: New scenario test exercising the Gemini vision path. Skipped when `GEMINI_API_KEY` is absent or when the API quota is exhausted (external quota issues should not fail CI).
- `web/components/markdown-renderer.tsx` *(new)*: Shared `MarkdownRenderer` component wrapping `react-markdown` with a `components` prop that applies explicit Tailwind utility classes (`list-decimal pl-5`, `list-disc pl-5`, `leading-relaxed`, etc.) when `proseEnabled` is true. Falls back to unstyled rendering when flag is off, preserving current behavior exactly.
- `api/main.py`: Added `markdown_prose_styling` key to `/flags` response, reading the `auto_markdown_prose_styling` Flagsmith flag (default off).
- `web/components/chat.tsx`: Replaced direct `ReactMarkdown` usage in bubble-layout and column-layout paths with `MarkdownRenderer`; reads new `markdown_prose_styling` flag and passes `proseEnabled` down.
- `web/components/recipe-card.tsx`: Updated fallback (non-recipe) rendering path to use `MarkdownRenderer` with `proseEnabled` prop instead of direct `ReactMarkdown`.

No new npm packages — uses only the already-installed `react-markdown`'s `components` prop.

## Flag

- `auto_multimodal_images` — default **off**. Enable in Flagsmith "cooking" project → Development to activate. When off, any `image` field in `/chat` requests is silently ignored.
- `auto_markdown_prose_styling` — default **off**. Enable in Flagsmith "cooking" project → Development to activate. When off, markdown renders as before (no visible change to existing behavior).

## Eval delta

Expand All @@ -24,33 +31,37 @@ Note: The operator focus hint specified Gemini 1.5 Flash, which has been retired
| dietary_constraints | ✅ 4/4 | ✅ 4/4 |
| safety_warning | ✅ 4/4 | ✅ 4/4 |
| substitution | ✅ 4/4 | ✅ 4/4 |
| multimodal_image (new) | — | ⏭️ skipped (Gemini quota exhausted during CI run; logic verified manually) |
| multimodal_image | ⏭️ skipped | ⏭️ skipped |

No regressions. No scenarios were modified.

## How to test

```bash
git checkout auto/improve-20260423-125402
git checkout auto/improve-20260423-134743
pip install -e ".[dev]"

# Backend
# Start backend
uvicorn api.main:app --port 8000

# Enable flag in Flagsmith: auto_multimodal_images → ON
# Then POST with an image:
curl -X POST http://localhost:8000/chat \
-H 'Content-Type: application/json' \
-d '{"message":"what can I cook?","image":"https://picsum.photos/id/429/400/300.jpg"}'
# Start frontend
cd web && npm install && npm run dev

# Enable flag in Flagsmith: auto_markdown_prose_styling → ON
# Open http://localhost:3000 and ask: "decide my dinner tonight"
# The agent will ask clarifying questions with numbered/bulleted lists
# → With flag ON: numbers and bullets are visible
# → With flag OFF: flat unstyled text (current broken behavior)

# Run scenarios
# Run scenarios (no backend flag change needed — pure frontend fix)
pytest -v tests/ -m agent_test
```

## Rollback

Flip `auto_multimodal_images` off in Flagsmith. No code revert needed — the entire image path is dead code when the flag is off.
Flip `auto_markdown_prose_styling` off in Flagsmith. No code revert needed — the entire styled path is a conditional branch off the flag.

## Follow-ups

- Add image input to the Legacy UI path (currently only wired to Premium UI)
- Add conversation history support for image messages (currently image turns don't participate in history)
- Consider streaming the Gemini response via SSE once `auto_streaming_response` flag is active
- Candidate 2: Fix multi-turn system prompt — `cooking_agent.py:96` uses base `SYSTEM_PROMPT` in the history path, silently dropping flag addendums (`auto_safety_check_enhanced`, etc.) for all multi-turn chats.
- Candidate 3: Add off-topic guardrail — trace `d92027fa` shows the agent answering questions about "light mode in this website" with ChatGPT instructions; the system prompt has no stay-on-topic rule.
21 changes: 15 additions & 6 deletions .github/_auto_scoreboard.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
# Auto Scoreboard — 2026-04-23

## Candidate Changes
## Evidence Summary
- 294 traces in last 7 days. No thumbs-down annotations, but real user complaints confirmed.
- **Trace `0ee0a635e5b25b7abe154d9e730c90ab`**: User said "I think the markdown is not rendering the numbers" — numbered lists invisible in chat UI.
- **Trace `dafdb4a5e3bbb0d1a6e12f2466ae1315`**: User asked for dinner options "like 1, 2, 3" — implying list rendering was broken.
- **Root cause confirmed in code**: `@tailwindcss/typography` is NOT in `web/package.json` and not in `tailwind.config.ts` plugins. Tailwind preflight resets `<ol>/<ul>` defaults (removes `list-style-type` and `padding`). The `prose-invert` class used throughout chat components has NO EFFECT without the plugin — numbered/bulleted lists render as unstyled flat text.
- **Trace `d92027fa5543cc5efda81de34f984462`**: Off-topic question ("where is light mode") received ChatGPT instructions — agent has no cooking-topic guardrail.
- **Code bug in `cooking_agent.py:96`**: Multi-turn history path uses base `SYSTEM_PROMPT` constant, ignoring flag addendums (`auto_safety_check_enhanced`, etc.).

## Candidates

| # | Title | Evidence | Impact | Risk | Rank |
|---|---|---|---|---|---|
| 1 | **Multimodal image input (Gemini 1.5 Flash)** | Operator focus hint. Zero image-related traces in 265 sampled — entirely absent capability. Fridge-photo → recipe and dish-recreation are highly natural user flows. `GEMINI_API_KEY` already wired in CI. | High | Med | **1st** |
| 2 | Add multi-turn conversation test scenario | Trace `59fd8cb8a61cb6ceb948a060977ed85b` shows multi-turn working but no scenario validates it. `auto_conversation_history` flag exists but is untested at the scenario level. | Med | Low | 2nd |
| 3 | Extend system prompt with fridge-inventory parsing | Several traces show users listing "what's in my fridge" style queries but the agent sometimes asks clarifying questions instead of immediately parsing. A structured inventory-extraction instruction would reduce latency. | Med | Med | 3rd |
|---|-------|----------|--------|------|------|
| 1 | **Fix markdown prose styling for non-recipe responses** | Traces `0ee0a635`, `dafdb4a5` — user-confirmed broken numbered lists. Root cause: `prose-invert` class non-functional without `@tailwindcss/typography`. Affects ALL non-recipe responses (numbered options, follow-up clarifications, substitution lists). | HIGH | LOW | **1** |
| 2 | **Fix multi-turn system prompt: apply flag addendums in history path** | `cooking_agent.py:96` — when `history` is present, uses base `SYSTEM_PROMPT` constant. Safety/dietary flags silently ignored for all multi-turn chats. | MED | LOW | 2 |
| 3 | **Add off-topic guardrail to system prompt** | Trace `d92027fa` — user asked about UI light mode; got ChatGPT instructions. No on-topic rule in SYSTEM_PROMPT. | MED | MED | 3 |

## Decision
**Candidate 1** — Fix markdown prose styling.

**Candidate 1 — multimodal image input** wins. Explicit `FOCUS` directive, entirely new capability with zero existing coverage, infrastructure (`GEMINI_API_KEY`, `httpx`) already in place.
User-confirmed complaint in traces, reproducible via code inspection, affects every numbered list and bullet response. Fix requires no new npm packages — just a `components` prop on ReactMarkdown and proper styling. Highest impact / lowest risk.
1 change: 1 addition & 0 deletions api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ def get_flags():
"premium_ui": flags.is_on("auto_premium_ui", default=False),
"session_threading": flags.is_on("auto_session_threading", default=False),
"multimodal_images": flags.is_on("auto_multimodal_images", default=False),
"markdown_prose_styling": flags.is_on("auto_markdown_prose_styling", default=False),
}


Expand Down
14 changes: 6 additions & 8 deletions web/components/chat.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
"use client";

import { useState, useRef, useEffect } from "react";
import ReactMarkdown from "react-markdown";
import { Send, Loader2, ChefHat, Sparkles, ImagePlus, X } from "lucide-react";
import { cn, API_URL } from "@/lib/utils";
import { RecipeCard } from "@/components/recipe-card";
import { MarkdownRenderer } from "@/components/markdown-renderer";

type Message = { role: "user" | "assistant"; content: string; image?: string };
type Tier = "cheap" | "mid" | "premium";
Expand Down Expand Up @@ -102,6 +102,7 @@ export default function Chat() {
const [sessionId, setSessionId] = useState<string>("");
const [activePrefs, setActivePrefs] = useState<Set<string>>(new Set());
const [multimodalEnabled, setMultimodalEnabled] = useState(false);
const [markdownProse, setMarkdownProse] = useState(false);
const [pendingImage, setPendingImage] = useState<string | null>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const scrollRef = useRef<HTMLDivElement>(null);
Expand All @@ -117,6 +118,7 @@ export default function Chat() {
setBubbleLayout(!!data?.chat_bubble_layout);
setPremiumUI(!!data?.premium_ui);
setMultimodalEnabled(!!data?.multimodal_images);
setMarkdownProse(!!data?.markdown_prose_styling);
const threading = !!data?.session_threading;
setSessionThreading(threading);
if (threading) {
Expand Down Expand Up @@ -305,7 +307,7 @@ export default function Chat() {
<span className="text-[10px] text-muted-foreground/60 pl-1 flex items-center gap-1">
<ChefHat size={9} /> Chef
</span>
<RecipeCard content={m.content} />
<RecipeCard content={m.content} proseEnabled={markdownProse} />
</div>
)
)}
Expand Down Expand Up @@ -470,17 +472,13 @@ export default function Chat() {
<div className={cn("text-xs font-semibold mb-1.5 tracking-wide", m.role === "user" ? "text-accent" : "text-muted-foreground")}>
{m.role === "user" ? "You" : "Chef"}
</div>
<div className="prose-invert">
<ReactMarkdown>{m.content}</ReactMarkdown>
</div>
<MarkdownRenderer content={m.content} proseEnabled={markdownProse} />
</div>
</div>
) : (
<div key={i} className={cn("rounded-lg px-4 py-3", m.role === "user" ? "bg-muted ml-8" : "bg-background mr-8 border border-border")}>
<div className="text-xs text-muted-foreground mb-1">{m.role === "user" ? "You" : "Chef"}</div>
<div className="prose-invert">
<ReactMarkdown>{m.content}</ReactMarkdown>
</div>
<MarkdownRenderer content={m.content} proseEnabled={markdownProse} />
</div>
)
)}
Expand Down
53 changes: 53 additions & 0 deletions web/components/markdown-renderer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
"use client";

import ReactMarkdown from "react-markdown";
import type { Components } from "react-markdown";

const styledComponents: Components = {
ol({ children }) {
return <ol className="list-decimal pl-5 space-y-1 my-2">{children}</ol>;
},
ul({ children }) {
return <ul className="list-disc pl-5 space-y-1 my-2">{children}</ul>;
},
li({ children }) {
return <li className="leading-relaxed">{children}</li>;
},
p({ children }) {
return <p className="my-1.5 leading-relaxed">{children}</p>;
},
strong({ children }) {
return <strong className="font-semibold text-foreground">{children}</strong>;
},
h1({ children }) {
return <h1 className="text-lg font-bold mt-3 mb-1.5">{children}</h1>;
},
h2({ children }) {
return <h2 className="text-base font-bold mt-2.5 mb-1">{children}</h2>;
},
h3({ children }) {
return <h3 className="text-sm font-semibold mt-2 mb-1">{children}</h3>;
},
code({ children }) {
return <code className="bg-muted px-1 py-0.5 rounded text-xs font-mono">{children}</code>;
},
hr() {
return <hr className="border-border/40 my-3" />;
},
};

export function MarkdownRenderer({
content,
proseEnabled,
}: {
content: string;
proseEnabled?: boolean;
}) {
return (
<div className="prose-invert text-sm leading-relaxed">
<ReactMarkdown components={proseEnabled ? styledComponents : undefined}>
{content}
</ReactMarkdown>
</div>
);
}
10 changes: 3 additions & 7 deletions web/components/recipe-card.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
"use client";

import { useState } from "react";
import ReactMarkdown from "react-markdown";
import { Check, ChefHat, Clock, Utensils } from "lucide-react";
import { cn } from "@/lib/utils";
import { parseRecipe, dietaryBadges } from "@/lib/parse-recipe";
import { MarkdownRenderer } from "@/components/markdown-renderer";

function badgeClass(badge: string): string {
const b = badge.toLowerCase();
Expand All @@ -17,18 +17,14 @@ function badgeClass(badge: string): string {
return "bg-muted/60 text-muted-foreground border-border/60";
}

export function RecipeCard({ content }: { content: string }) {
export function RecipeCard({ content, proseEnabled }: { content: string; proseEnabled?: boolean }) {
const recipe = parseRecipe(content);

const [checkedIngredients, setCheckedIngredients] = useState<Set<string>>(new Set());
const [completedSteps, setCompletedSteps] = useState<Set<number>>(new Set());

if (!recipe) {
return (
<div className="prose-invert">
<ReactMarkdown>{content}</ReactMarkdown>
</div>
);
return <MarkdownRenderer content={content} proseEnabled={proseEnabled} />;
}

const badges = recipe.dietaryInfo ? dietaryBadges(recipe.dietaryInfo) : [];
Expand Down
Loading