Skip to content

Commit dd17e96

Browse files
github-actions[bot]langwatch-improve-bot
andauthored
auto: iteration 20260423-134743 (#18)
Co-authored-by: langwatch-improve-bot <bot@langwatch.ai>
1 parent ebbca60 commit dd17e96

6 files changed

Lines changed: 109 additions & 41 deletions

File tree

‎.github/_auto_pr_body.md‎

Lines changed: 31 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,27 @@
1-
# auto: add multimodal image input via Gemini 2.0 Flash
1+
# auto: fix markdown prose styling for non-recipe chat responses
22

33
## Why
44

5-
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).
5+
Two real user traces confirm that numbered lists and bullet points are invisible in the chat UI:
6+
- Trace `0ee0a635e5b25b7abe154d9e730c90ab`: User said "I think the markdown is not rendering the numbers" after receiving a numbered dinner-options list.
7+
- Trace `dafdb4a5e3bbb0d1a6e12f2466ae1315`: User asked the agent to format options "like 1, 2, 3" — implying current list rendering was broken.
68

7-
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.
9+
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.
10+
11+
This affects every non-recipe response: numbered dinner options, clarifying-question lists, substitution bullet points, multi-turn follow-ups.
812

913
## What
1014

11-
- `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.
12-
- `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.
13-
- `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).
15+
- `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.
16+
- `api/main.py`: Added `markdown_prose_styling` key to `/flags` response, reading the `auto_markdown_prose_styling` Flagsmith flag (default off).
17+
- `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.
18+
- `web/components/recipe-card.tsx`: Updated fallback (non-recipe) rendering path to use `MarkdownRenderer` with `proseEnabled` prop instead of direct `ReactMarkdown`.
19+
20+
No new npm packages — uses only the already-installed `react-markdown`'s `components` prop.
1421

1522
## Flag
1623

17-
- `auto_multimodal_images` — default **off**. Enable in Flagsmith "cooking" project → Development to activate. When off, any `image` field in `/chat` requests is silently ignored.
24+
- `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).
1825

1926
## Eval delta
2027

@@ -24,33 +31,37 @@ Note: The operator focus hint specified Gemini 1.5 Flash, which has been retired
2431
| dietary_constraints | ✅ 4/4 | ✅ 4/4 |
2532
| safety_warning | ✅ 4/4 | ✅ 4/4 |
2633
| substitution | ✅ 4/4 | ✅ 4/4 |
27-
| multimodal_image (new) | — | ⏭️ skipped (Gemini quota exhausted during CI run; logic verified manually) |
34+
| multimodal_image | ⏭️ skipped | ⏭️ skipped |
35+
36+
No regressions. No scenarios were modified.
2837

2938
## How to test
3039

3140
```bash
32-
git checkout auto/improve-20260423-125402
41+
git checkout auto/improve-20260423-134743
3342
pip install -e ".[dev]"
3443

35-
# Backend
44+
# Start backend
3645
uvicorn api.main:app --port 8000
3746

38-
# Enable flag in Flagsmith: auto_multimodal_images → ON
39-
# Then POST with an image:
40-
curl -X POST http://localhost:8000/chat \
41-
-H 'Content-Type: application/json' \
42-
-d '{"message":"what can I cook?","image":"https://picsum.photos/id/429/400/300.jpg"}'
47+
# Start frontend
48+
cd web && npm install && npm run dev
49+
50+
# Enable flag in Flagsmith: auto_markdown_prose_styling → ON
51+
# Open http://localhost:3000 and ask: "decide my dinner tonight"
52+
# The agent will ask clarifying questions with numbered/bulleted lists
53+
# → With flag ON: numbers and bullets are visible
54+
# → With flag OFF: flat unstyled text (current broken behavior)
4355

44-
# Run scenarios
56+
# Run scenarios (no backend flag change needed — pure frontend fix)
4557
pytest -v tests/ -m agent_test
4658
```
4759

4860
## Rollback
4961

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

5264
## Follow-ups
5365

54-
- Add image input to the Legacy UI path (currently only wired to Premium UI)
55-
- Add conversation history support for image messages (currently image turns don't participate in history)
56-
- Consider streaming the Gemini response via SSE once `auto_streaming_response` flag is active
66+
- 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.
67+
- 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.

‎.github/_auto_scoreboard.md‎

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,22 @@
11
# Auto Scoreboard — 2026-04-23
22

3-
## Candidate Changes
3+
## Evidence Summary
4+
- 294 traces in last 7 days. No thumbs-down annotations, but real user complaints confirmed.
5+
- **Trace `0ee0a635e5b25b7abe154d9e730c90ab`**: User said "I think the markdown is not rendering the numbers" — numbered lists invisible in chat UI.
6+
- **Trace `dafdb4a5e3bbb0d1a6e12f2466ae1315`**: User asked for dinner options "like 1, 2, 3" — implying list rendering was broken.
7+
- **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.
8+
- **Trace `d92027fa5543cc5efda81de34f984462`**: Off-topic question ("where is light mode") received ChatGPT instructions — agent has no cooking-topic guardrail.
9+
- **Code bug in `cooking_agent.py:96`**: Multi-turn history path uses base `SYSTEM_PROMPT` constant, ignoring flag addendums (`auto_safety_check_enhanced`, etc.).
10+
11+
## Candidates
412

513
| # | Title | Evidence | Impact | Risk | Rank |
6-
|---|---|---|---|---|---|
7-
| 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** |
8-
| 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 |
9-
| 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 |
14+
|---|-------|----------|--------|------|------|
15+
| 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** |
16+
| 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 |
17+
| 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 |
1018

1119
## Decision
20+
**Candidate 1** — Fix markdown prose styling.
1221

13-
**Candidate 1 — multimodal image input** wins. Explicit `FOCUS` directive, entirely new capability with zero existing coverage, infrastructure (`GEMINI_API_KEY`, `httpx`) already in place.
22+
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.

‎api/main.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ def get_flags():
135135
"premium_ui": flags.is_on("auto_premium_ui", default=False),
136136
"session_threading": flags.is_on("auto_session_threading", default=False),
137137
"multimodal_images": flags.is_on("auto_multimodal_images", default=False),
138+
"markdown_prose_styling": flags.is_on("auto_markdown_prose_styling", default=False),
138139
}
139140

140141

‎web/components/chat.tsx‎

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
"use client";
22

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

99
type Message = { role: "user" | "assistant"; content: string; image?: string };
1010
type Tier = "cheap" | "mid" | "premium";
@@ -102,6 +102,7 @@ export default function Chat() {
102102
const [sessionId, setSessionId] = useState<string>("");
103103
const [activePrefs, setActivePrefs] = useState<Set<string>>(new Set());
104104
const [multimodalEnabled, setMultimodalEnabled] = useState(false);
105+
const [markdownProse, setMarkdownProse] = useState(false);
105106
const [pendingImage, setPendingImage] = useState<string | null>(null);
106107
const fileInputRef = useRef<HTMLInputElement>(null);
107108
const scrollRef = useRef<HTMLDivElement>(null);
@@ -117,6 +118,7 @@ export default function Chat() {
117118
setBubbleLayout(!!data?.chat_bubble_layout);
118119
setPremiumUI(!!data?.premium_ui);
119120
setMultimodalEnabled(!!data?.multimodal_images);
121+
setMarkdownProse(!!data?.markdown_prose_styling);
120122
const threading = !!data?.session_threading;
121123
setSessionThreading(threading);
122124
if (threading) {
@@ -305,7 +307,7 @@ export default function Chat() {
305307
<span className="text-[10px] text-muted-foreground/60 pl-1 flex items-center gap-1">
306308
<ChefHat size={9} /> Chef
307309
</span>
308-
<RecipeCard content={m.content} />
310+
<RecipeCard content={m.content} proseEnabled={markdownProse} />
309311
</div>
310312
)
311313
)}
@@ -470,17 +472,13 @@ export default function Chat() {
470472
<div className={cn("text-xs font-semibold mb-1.5 tracking-wide", m.role === "user" ? "text-accent" : "text-muted-foreground")}>
471473
{m.role === "user" ? "You" : "Chef"}
472474
</div>
473-
<div className="prose-invert">
474-
<ReactMarkdown>{m.content}</ReactMarkdown>
475-
</div>
475+
<MarkdownRenderer content={m.content} proseEnabled={markdownProse} />
476476
</div>
477477
</div>
478478
) : (
479479
<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")}>
480480
<div className="text-xs text-muted-foreground mb-1">{m.role === "user" ? "You" : "Chef"}</div>
481-
<div className="prose-invert">
482-
<ReactMarkdown>{m.content}</ReactMarkdown>
483-
</div>
481+
<MarkdownRenderer content={m.content} proseEnabled={markdownProse} />
484482
</div>
485483
)
486484
)}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
"use client";
2+
3+
import ReactMarkdown from "react-markdown";
4+
import type { Components } from "react-markdown";
5+
6+
const styledComponents: Components = {
7+
ol({ children }) {
8+
return <ol className="list-decimal pl-5 space-y-1 my-2">{children}</ol>;
9+
},
10+
ul({ children }) {
11+
return <ul className="list-disc pl-5 space-y-1 my-2">{children}</ul>;
12+
},
13+
li({ children }) {
14+
return <li className="leading-relaxed">{children}</li>;
15+
},
16+
p({ children }) {
17+
return <p className="my-1.5 leading-relaxed">{children}</p>;
18+
},
19+
strong({ children }) {
20+
return <strong className="font-semibold text-foreground">{children}</strong>;
21+
},
22+
h1({ children }) {
23+
return <h1 className="text-lg font-bold mt-3 mb-1.5">{children}</h1>;
24+
},
25+
h2({ children }) {
26+
return <h2 className="text-base font-bold mt-2.5 mb-1">{children}</h2>;
27+
},
28+
h3({ children }) {
29+
return <h3 className="text-sm font-semibold mt-2 mb-1">{children}</h3>;
30+
},
31+
code({ children }) {
32+
return <code className="bg-muted px-1 py-0.5 rounded text-xs font-mono">{children}</code>;
33+
},
34+
hr() {
35+
return <hr className="border-border/40 my-3" />;
36+
},
37+
};
38+
39+
export function MarkdownRenderer({
40+
content,
41+
proseEnabled,
42+
}: {
43+
content: string;
44+
proseEnabled?: boolean;
45+
}) {
46+
return (
47+
<div className="prose-invert text-sm leading-relaxed">
48+
<ReactMarkdown components={proseEnabled ? styledComponents : undefined}>
49+
{content}
50+
</ReactMarkdown>
51+
</div>
52+
);
53+
}

‎web/components/recipe-card.tsx‎

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
"use client";
22

33
import { useState } from "react";
4-
import ReactMarkdown from "react-markdown";
54
import { Check, ChefHat, Clock, Utensils } from "lucide-react";
65
import { cn } from "@/lib/utils";
76
import { parseRecipe, dietaryBadges } from "@/lib/parse-recipe";
7+
import { MarkdownRenderer } from "@/components/markdown-renderer";
88

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

20-
export function RecipeCard({ content }: { content: string }) {
20+
export function RecipeCard({ content, proseEnabled }: { content: string; proseEnabled?: boolean }) {
2121
const recipe = parseRecipe(content);
2222

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

2626
if (!recipe) {
27-
return (
28-
<div className="prose-invert">
29-
<ReactMarkdown>{content}</ReactMarkdown>
30-
</div>
31-
);
27+
return <MarkdownRenderer content={content} proseEnabled={proseEnabled} />;
3228
}
3329

3430
const badges = recipe.dietaryInfo ? dietaryBadges(recipe.dietaryInfo) : [];

0 commit comments

Comments
 (0)