ZENLLC commited on
Commit
dad0db3
·
verified ·
1 Parent(s): 8e6dc1c

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +689 -0
app.py ADDED
@@ -0,0 +1,689 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import base64
3
+ from io import BytesIO
4
+ from typing import List, Dict, Optional, Tuple
5
+
6
+ import gradio as gr
7
+ from openai import OpenAI
8
+ from PIL import Image
9
+
10
+ # -------------------------------------------------------------------
11
+ # App Metadata
12
+ # -------------------------------------------------------------------
13
+
14
+ APP_TITLE = "ZEN AI Co. Module 2 | Agent Assembler"
15
+ APP_DESCRIPTION = """
16
+ OpenAI-only teaching rig for building AI model UIs.
17
+
18
+ • Uses GPT-5 for text generation.
19
+ • Uses DALL·E 3 (with fallback to gpt-image-1) for image generation.
20
+ • Lets you edit the system prompt, role, tone, and output format.
21
+ • Provides sliders and controls to experiment with behavior.
22
+ • Automatically generates images when the user asks for one, with an option
23
+ to always generate images as well.
24
+ """
25
+
26
+ DEFAULT_TEMPERATURE = 0.7
27
+ DEFAULT_TOP_P = 1.0
28
+ DEFAULT_MAX_TOKENS = 1024
29
+ DEFAULT_PRESENCE_PENALTY = 0.0
30
+ DEFAULT_FREQUENCY_PENALTY = 0.0
31
+
32
+ # -------------------------------------------------------------------
33
+ # OpenAI Client Helper
34
+ # -------------------------------------------------------------------
35
+
36
+ def get_openai_client(key_override: Optional[str] = None) -> OpenAI:
37
+ """
38
+ Returns an OpenAI client using either:
39
+ 1) key from the UI override, or
40
+ 2) OPENAI_API_KEY environment variable.
41
+ """
42
+ api_key = (key_override or "").strip() or os.getenv("OPENAI_API_KEY", "").strip()
43
+ if not api_key:
44
+ raise ValueError(
45
+ "OpenAI API key missing. Set OPENAI_API_KEY env var "
46
+ "or paste it into the sidebar."
47
+ )
48
+ return OpenAI(api_key=api_key)
49
+
50
+ # -------------------------------------------------------------------
51
+ # Prompt & Style Helpers
52
+ # -------------------------------------------------------------------
53
+
54
+ def build_system_instructions(
55
+ user_system_prompt: str,
56
+ assistant_role: str,
57
+ output_mode: str,
58
+ tone: str,
59
+ temperature: float,
60
+ top_p: float,
61
+ presence_penalty: float,
62
+ frequency_penalty: float,
63
+ ) -> str:
64
+ """
65
+ Build a system prompt string combining user-provided base instructions
66
+ with role + format + tone + "virtual sampling" metadata.
67
+
68
+ We encode the slider settings as behavior hints because some GPT-5 variants
69
+ do not accept temperature/top_p/penalties as API parameters.
70
+ """
71
+ role_map = {
72
+ "General Assistant": "Behave as a highly capable, calm general-purpose AI assistant.",
73
+ "Teacher / Instructor": "Behave as a patient educator. Explain concepts step-by-step and check for understanding.",
74
+ "Engineer / Architect": "Behave as a senior engineer and systems architect. Be explicit, structured, and precise.",
75
+ "Storyteller / Creative": "Behave as a creative storyteller. Use vivid but clear language while staying coherent.",
76
+ }
77
+
78
+ output_map = {
79
+ "Standard Chat": "Respond like a normal chat, with paragraphs kept short and skimmable.",
80
+ "Executive Report": (
81
+ "Respond as an executive brief with headings, short sections, and bullet points "
82
+ "highlighting key decisions and risks."
83
+ ),
84
+ "Infographic Outline": (
85
+ "Respond as an infographic blueprint with section titles and short bullet lines. "
86
+ "Focus on clarity and label-friendly phrases."
87
+ ),
88
+ "Bullet Summary": (
89
+ "Respond as a tight bullet summary (5–10 bullets) capturing only the most important details."
90
+ ),
91
+ }
92
+
93
+ tone_map = {
94
+ "Neutral": "Keep the tone neutral and globally understandable.",
95
+ "Bold / Visionary": "Use confident, forward-looking language, but stay concrete and honest.",
96
+ "Minimalist": "Be extremely concise. Prefer fewer words and high information density.",
97
+ }
98
+
99
+ sampling_hint = (
100
+ "SAMPLING HINTS FROM UI SLIDERS:\n"
101
+ f"- Temperature slider: {temperature:.2f} (higher = more creative and speculative).\n"
102
+ f"- Top-p slider: {top_p:.2f} (lower = more conservative).\n"
103
+ f"- Presence penalty slider: {presence_penalty:.2f} (higher = encourage new topics).\n"
104
+ f"- Frequency penalty slider: {frequency_penalty:.2f} (higher = reduce repetition).\n"
105
+ "You must interpret these values as behavioral guidance even if the underlying "
106
+ "model ignores sampling parameters."
107
+ )
108
+
109
+ parts = [
110
+ (user_system_prompt or "").strip(),
111
+ "",
112
+ f"ASSISTANT ROLE: {role_map.get(assistant_role, '')}",
113
+ f"OUTPUT MODE: {output_map.get(output_mode, '')}",
114
+ f"TONE: {tone_map.get(tone, '')}",
115
+ "",
116
+ sampling_hint,
117
+ "",
118
+ "Always output clean Markdown.",
119
+ ]
120
+ return "\n".join(p for p in parts if p.strip())
121
+
122
+
123
+ def history_to_openai_messages(
124
+ history_messages: List[Dict[str, str]],
125
+ user_message: str,
126
+ system_instructions: str,
127
+ ) -> List[Dict[str, str]]:
128
+ """
129
+ Convert Chatbot-style history (list of {role, content}) into an OpenAI
130
+ messages list with an added system message and new user query.
131
+ """
132
+ messages: List[Dict[str, str]] = []
133
+ if system_instructions:
134
+ messages.append({"role": "system", "content": system_instructions})
135
+
136
+ # Reuse existing history
137
+ for msg in history_messages:
138
+ role = msg.get("role")
139
+ content = msg.get("content", "")
140
+ if role in ("user", "assistant", "system") and content:
141
+ messages.append({"role": role, "content": content})
142
+
143
+ # New query
144
+ messages.append({"role": "user", "content": user_message})
145
+ return messages
146
+
147
+ # -------------------------------------------------------------------
148
+ # Text & Image Generation Helpers
149
+ # -------------------------------------------------------------------
150
+
151
+ def call_openai_text(
152
+ openai_key: Optional[str],
153
+ messages: List[Dict[str, str]],
154
+ max_tokens: int,
155
+ ) -> str:
156
+ """
157
+ Call GPT-5 via Chat Completions using only supported parameters:
158
+ - model
159
+ - messages
160
+ - max_completion_tokens
161
+ """
162
+ client = get_openai_client(openai_key)
163
+ completion = client.chat.completions.create(
164
+ model="gpt-5", # change to exact variant you have (e.g. "gpt-5.1-mini") if needed
165
+ messages=messages,
166
+ max_completion_tokens=max_tokens,
167
+ )
168
+ return completion.choices[0].message.content
169
+
170
+
171
+ def call_openai_image_with_fallback(
172
+ openai_key: Optional[str],
173
+ prompt: str,
174
+ size: str = "1024x1024",
175
+ ) -> Optional[Image.Image]:
176
+ """
177
+ Try DALL·E 3 first. If it fails, fall back to gpt-image-1.
178
+ We explicitly request base64 output and handle missing b64_json safely.
179
+ """
180
+ client = get_openai_client(openai_key)
181
+ last_error: Optional[Exception] = None
182
+
183
+ for model_name in ["dall-e-3", "gpt-image-1"]:
184
+ try:
185
+ response = client.images.generate(
186
+ model=model_name,
187
+ prompt=prompt,
188
+ size=size,
189
+ n=1,
190
+ quality="hd", # high quality
191
+ response_format="b64_json", # ensure base64 output
192
+ )
193
+ if not response.data:
194
+ continue
195
+
196
+ b64 = getattr(response.data[0], "b64_json", None)
197
+ if not b64:
198
+ # No base64 data; try next model
199
+ continue
200
+
201
+ img_bytes = base64.b64decode(b64)
202
+ return Image.open(BytesIO(img_bytes))
203
+ except Exception as e:
204
+ last_error = e
205
+ # Try next model in the list if available
206
+ continue
207
+
208
+ if last_error:
209
+ # Bubble up the last error so caller can log it or display a message
210
+ raise last_error
211
+
212
+ return None
213
+
214
+ # -------------------------------------------------------------------
215
+ # Starter Prompts
216
+ # -------------------------------------------------------------------
217
+
218
+ STARTER_PROMPTS = {
219
+ "Explain AI Literacy to a 13-year-old":
220
+ "Explain what AI literacy is to a 13-year-old who loves YouTube and video games. "
221
+ "Use examples from their world and end with 3 practical things they can do this week.",
222
+
223
+ "Executive Brief: AI Strategy for a Nonprofit":
224
+ "Create an executive brief for a youth-serving nonprofit that wants to adopt AI tools. "
225
+ "Include priorities, risks, and quick wins in under 800 words.",
226
+
227
+ "Infographic Outline: ZEN AI Pioneer Program":
228
+ "Create an infographic outline that explains the ZEN AI Pioneer Program: "
229
+ "what it is, who it serves, what makes it historic, and 3 key stats. "
230
+ "Make the sections short and label-friendly.",
231
+
232
+ "Creative Image Prompt: Futuristic ZEN AI Lab":
233
+ "Describe a futuristic but realistic ZEN AI Co. lab where youth are building their own AI tools. "
234
+ "Focus on what the scene looks like so it can be turned into an illustration. "
235
+ "End with a separate final paragraph that is ONLY the pure image prompt text.",
236
+
237
+ "Debugging Prompt: Why is my model hallucinating?":
238
+ "I built a small AI app and the model is hallucinating facts about my organization. "
239
+ "Explain why that happens and propose a 3-layer mitigation strategy (prompting, retrieval, UX).",
240
+ }
241
+
242
+ def get_starter_prompt(choice: str) -> str:
243
+ return STARTER_PROMPTS.get(choice, "")
244
+
245
+ # -------------------------------------------------------------------
246
+ # Image Intent Detection
247
+ # -------------------------------------------------------------------
248
+
249
+ def wants_image_from_text(text: str) -> bool:
250
+ """
251
+ Heuristic to decide if the user is asking for an image.
252
+
253
+ Triggers on phrases like:
254
+ - "generate an image"
255
+ - "create an image"
256
+ - "make an image"
257
+ - "image of"
258
+ - "picture of"
259
+ - "draw"
260
+ - "illustration"
261
+ - "infographic"
262
+ - "poster"
263
+ - "logo"
264
+ - "cover art"
265
+ - "thumbnail"
266
+
267
+ But avoids when user explicitly says they do NOT want an image.
268
+ """
269
+ t = text.lower()
270
+
271
+ # Negative patterns
272
+ negative_patterns = [
273
+ "don't generate an image",
274
+ "dont generate an image",
275
+ "don't create an image",
276
+ "dont create an image",
277
+ "no image",
278
+ "no images",
279
+ "without an image",
280
+ ]
281
+ if any(p in t for p in negative_patterns):
282
+ return False
283
+
284
+ positive_patterns = [
285
+ "generate an image",
286
+ "create an image",
287
+ "make an image",
288
+ "generate a picture",
289
+ "create a picture",
290
+ "picture of",
291
+ "image of",
292
+ "draw ",
293
+ "draw an",
294
+ "draw a",
295
+ "illustration",
296
+ "infographic",
297
+ "poster",
298
+ "logo",
299
+ "cover art",
300
+ "thumbnail",
301
+ "album art",
302
+ ]
303
+
304
+ return any(p in t for p in positive_patterns)
305
+
306
+ # -------------------------------------------------------------------
307
+ # Core Chat Logic
308
+ # -------------------------------------------------------------------
309
+
310
+ def agent_assembler_chat(
311
+ user_message: str,
312
+ chat_history: List[Dict[str, str]],
313
+ openai_key_ui: str,
314
+ system_prompt_ui: str,
315
+ assistant_role: str,
316
+ output_mode: str,
317
+ tone: str,
318
+ temperature: float,
319
+ top_p: float,
320
+ max_tokens: int,
321
+ presence_penalty: float,
322
+ frequency_penalty: float,
323
+ always_generate_image: bool,
324
+ image_style: str,
325
+ image_aspect: str,
326
+ ) -> Tuple[List[Dict[str, str]], Optional[Image.Image]]:
327
+ """
328
+ Main callback: GPT-5 text + optional image generation.
329
+
330
+ - Detects image intent from user text automatically.
331
+ - Optionally always generates an image if the toggle is on.
332
+ - chat_history is a list of messages: [{role, content}, ...]
333
+ """
334
+ if not user_message.strip():
335
+ return chat_history, None
336
+
337
+ # Build system instructions (including slider hints)
338
+ system_instructions = build_system_instructions(
339
+ user_system_prompt=system_prompt_ui,
340
+ assistant_role=assistant_role,
341
+ output_mode=output_mode,
342
+ tone=tone,
343
+ temperature=temperature,
344
+ top_p=top_p,
345
+ presence_penalty=presence_penalty,
346
+ frequency_penalty=frequency_penalty,
347
+ )
348
+
349
+ # Prepare messages for OpenAI
350
+ openai_messages = history_to_openai_messages(
351
+ history_messages=chat_history,
352
+ user_message=user_message,
353
+ system_instructions=system_instructions,
354
+ )
355
+
356
+ # Call GPT-5
357
+ try:
358
+ ai_reply = call_openai_text(
359
+ openai_key=openai_key_ui,
360
+ messages=openai_messages,
361
+ max_tokens=max_tokens,
362
+ )
363
+ except Exception as e:
364
+ ai_reply = (
365
+ "There was an error calling GPT-5.\n\n"
366
+ f"Short message: `{e}`\n\n"
367
+ "Check that your API key is valid and that the model name matches "
368
+ "what is available in your OpenAI account."
369
+ )
370
+
371
+ # Update history
372
+ chat_history = chat_history + [
373
+ {"role": "user", "content": user_message},
374
+ {"role": "assistant", "content": ai_reply},
375
+ ]
376
+
377
+ # Decide whether to generate an image
378
+ auto_image = wants_image_from_text(user_message)
379
+ should_generate_image = always_generate_image or auto_image
380
+
381
+ generated_image: Optional[Image.Image] = None
382
+ if should_generate_image:
383
+ # Map aspect label to image size
384
+ aspect_to_size = {
385
+ "Square (1:1)": "1024x1024",
386
+ "Portrait (9:16)": "1024x1792",
387
+ "Landscape (16:9)": "1792x1024",
388
+ }
389
+ size = aspect_to_size.get(image_aspect, "1024x1024")
390
+
391
+ # Build image prompt
392
+ image_prompt = (
393
+ f"{user_message.strip()}\n\n"
394
+ f"IMAGE STYLE: {image_style}. "
395
+ "High readability, clean composition, suitable for presentations or infographics."
396
+ )
397
+
398
+ try:
399
+ generated_image = call_openai_image_with_fallback(
400
+ openai_key=openai_key_ui,
401
+ prompt=image_prompt,
402
+ size=size,
403
+ )
404
+ if generated_image is None:
405
+ # No explicit exception but no image either
406
+ if chat_history and chat_history[-1].get("role") == "assistant":
407
+ chat_history[-1]["content"] += (
408
+ "\n\n_Image generation returned no data. "
409
+ "Check your OpenAI key and image model availability._"
410
+ )
411
+ except Exception as e:
412
+ # Attach error note to latest assistant message
413
+ if chat_history and chat_history[-1].get("role") == "assistant":
414
+ chat_history[-1]["content"] += (
415
+ f"\n\n_Image generation failed: `{e}`. "
416
+ "Check your OpenAI key and dalle-3 / gpt-image-1 availability._"
417
+ )
418
+
419
+ return chat_history, generated_image
420
+
421
+
422
+ def clear_chat() -> Tuple[List[Dict[str, str]], Optional[Image.Image]]:
423
+ """
424
+ Clear chat and image.
425
+ """
426
+ return [], None
427
+
428
+ # -------------------------------------------------------------------
429
+ # Gradio UI
430
+ # -------------------------------------------------------------------
431
+
432
+ DEFAULT_SYSTEM_PROMPT = """You are ZEN AI Co.'s Agent Assembler.
433
+
434
+ You help people understand how large language models and image models work
435
+ by giving clear, practical, and honest answers. You are allowed to:
436
+
437
+ - Explain how prompts, system messages, and parameters change behavior.
438
+ - Suggest better prompts and show before/after improvements.
439
+ - Design prompts for text and images that are copy-paste-ready.
440
+ - Produce reports, outlines, and infographic blueprints.
441
+
442
+ You must:
443
+
444
+ - Avoid making up API capabilities that do not exist.
445
+ - Be honest when you don't know something or lack context.
446
+ - Keep spelling and formatting very precise, especially in prompts and labels.
447
+ """
448
+
449
+ def build_interface() -> gr.Blocks:
450
+ with gr.Blocks(title=APP_TITLE) as demo:
451
+ gr.Markdown(f"# {APP_TITLE}")
452
+ gr.Markdown(APP_DESCRIPTION)
453
+
454
+ with gr.Row():
455
+ # LEFT: Chat + image + input
456
+ with gr.Column(scale=3):
457
+ chatbot = gr.Chatbot(
458
+ label="Agent Assembler Chat",
459
+ height=520,
460
+ )
461
+ image_out = gr.Image(
462
+ label="Latest Generated Image (DALL·E 3 / gpt-image-1)",
463
+ height=320,
464
+ interactive=False,
465
+ )
466
+
467
+ user_input = gr.Textbox(
468
+ label="Your message",
469
+ placeholder="Ask a question, request a report, or describe a scene for image generation...",
470
+ lines=3,
471
+ )
472
+
473
+ with gr.Row():
474
+ send_btn = gr.Button("Send", variant="primary")
475
+ clear_btn = gr.Button("Clear Chat")
476
+
477
+ gr.Markdown("### Starter Prompts")
478
+ starter_choice = gr.Dropdown(
479
+ label="Pick a starter prompt to auto-fill the input",
480
+ choices=list(STARTER_PROMPTS.keys()),
481
+ value="Explain AI Literacy to a 13-year-old",
482
+ )
483
+ starter_btn = gr.Button("Load Starter Prompt")
484
+
485
+ # RIGHT: Controls / teaching panel
486
+ with gr.Column(scale=2):
487
+ gr.Markdown("## API & System Prompt")
488
+
489
+ openai_key_ui = gr.Textbox(
490
+ label="OpenAI API Key (optional; otherwise uses OPENAI_API_KEY env var)",
491
+ type="password",
492
+ )
493
+
494
+ system_prompt_ui = gr.Textbox(
495
+ label="System / Model Instructions",
496
+ value=DEFAULT_SYSTEM_PROMPT,
497
+ lines=10,
498
+ )
499
+
500
+ gr.Markdown("## Behavior & Style")
501
+
502
+ assistant_role = gr.Radio(
503
+ label="Assistant Role",
504
+ choices=[
505
+ "General Assistant",
506
+ "Teacher / Instructor",
507
+ "Engineer / Architect",
508
+ "Storyteller / Creative",
509
+ ],
510
+ value="General Assistant",
511
+ )
512
+
513
+ output_mode = gr.Radio(
514
+ label="Output Format",
515
+ choices=[
516
+ "Standard Chat",
517
+ "Executive Report",
518
+ "Infographic Outline",
519
+ "Bullet Summary",
520
+ ],
521
+ value="Standard Chat",
522
+ )
523
+
524
+ tone = gr.Radio(
525
+ label="Tone",
526
+ choices=[
527
+ "Neutral",
528
+ "Bold / Visionary",
529
+ "Minimalist",
530
+ ],
531
+ value="Neutral",
532
+ )
533
+
534
+ gr.Markdown("## Sampling (Experiment Zone)\n"
535
+ "These are teaching controls; for some GPT-5 variants they only influence behavior via the system prompt.")
536
+
537
+ temperature = gr.Slider(
538
+ label="Temperature (creativity / randomness)",
539
+ minimum=0.0,
540
+ maximum=1.5,
541
+ value=DEFAULT_TEMPERATURE,
542
+ step=0.05,
543
+ )
544
+
545
+ top_p = gr.Slider(
546
+ label="Top-p (nucleus sampling)",
547
+ minimum=0.1,
548
+ maximum=1.0,
549
+ value=DEFAULT_TOP_P,
550
+ step=0.05,
551
+ )
552
+
553
+ max_tokens = gr.Slider(
554
+ label="Max completion tokens",
555
+ minimum=128,
556
+ maximum=4096,
557
+ value=DEFAULT_MAX_TOKENS,
558
+ step=128,
559
+ )
560
+
561
+ presence_penalty = gr.Slider(
562
+ label="Presence penalty (encourage new topics)",
563
+ minimum=-2.0,
564
+ maximum=2.0,
565
+ value=DEFAULT_PRESENCE_PENALTY,
566
+ step=0.1,
567
+ )
568
+
569
+ frequency_penalty = gr.Slider(
570
+ label="Frequency penalty (discourage repetition)",
571
+ minimum=-2.0,
572
+ maximum=2.0,
573
+ value=DEFAULT_FREQUENCY_PENALTY,
574
+ step=0.1,
575
+ )
576
+
577
+ gr.Markdown("## Image Generation")
578
+
579
+ always_generate_image = gr.Checkbox(
580
+ label="Always generate an image for each message (in addition to auto-detect intent)",
581
+ value=False,
582
+ )
583
+
584
+ image_style = gr.Radio(
585
+ label="Image Style",
586
+ choices=[
587
+ "Futuristic glass UI dashboard",
588
+ "Clean infographic illustration",
589
+ "Soft watercolor concept art",
590
+ "High-contrast comic / graphic novel",
591
+ "Photorealistic lab / studio scene",
592
+ ],
593
+ value="Clean infographic illustration",
594
+ )
595
+
596
+ image_aspect = gr.Radio(
597
+ label="Aspect Ratio",
598
+ choices=[
599
+ "Square (1:1)",
600
+ "Portrait (9:16)",
601
+ "Landscape (16:9)",
602
+ ],
603
+ value="Square (1:1)",
604
+ )
605
+
606
+ # Shared chat state: list of messages (dicts)
607
+ chat_state = gr.State([])
608
+
609
+ # Send button: main call
610
+ send_btn.click(
611
+ fn=agent_assembler_chat,
612
+ inputs=[
613
+ user_input,
614
+ chat_state,
615
+ openai_key_ui,
616
+ system_prompt_ui,
617
+ assistant_role,
618
+ output_mode,
619
+ tone,
620
+ temperature,
621
+ top_p,
622
+ max_tokens,
623
+ presence_penalty,
624
+ frequency_penalty,
625
+ always_generate_image,
626
+ image_style,
627
+ image_aspect,
628
+ ],
629
+ outputs=[chatbot, image_out],
630
+ ).then(
631
+ fn=lambda msgs: (msgs, ""), # sync state, clear input
632
+ inputs=chatbot,
633
+ outputs=[chat_state, user_input],
634
+ )
635
+
636
+ # Submit on Enter
637
+ user_input.submit(
638
+ fn=agent_assembler_chat,
639
+ inputs=[
640
+ user_input,
641
+ chat_state,
642
+ openai_key_ui,
643
+ system_prompt_ui,
644
+ assistant_role,
645
+ output_mode,
646
+ tone,
647
+ temperature,
648
+ top_p,
649
+ max_tokens,
650
+ presence_penalty,
651
+ frequency_penalty,
652
+ always_generate_image,
653
+ image_style,
654
+ image_aspect,
655
+ ],
656
+ outputs=[chatbot, image_out],
657
+ ).then(
658
+ fn=lambda msgs: (msgs, ""),
659
+ inputs=chatbot,
660
+ outputs=[chat_state, user_input],
661
+ )
662
+
663
+ # Clear chat
664
+ clear_btn.click(
665
+ fn=clear_chat,
666
+ inputs=None,
667
+ outputs=[chatbot, image_out],
668
+ ).then(
669
+ fn=lambda: [],
670
+ inputs=None,
671
+ outputs=chat_state,
672
+ )
673
+
674
+ # Starter prompt loader
675
+ def _load_starter(choice: str) -> str:
676
+ return get_starter_prompt(choice)
677
+
678
+ starter_btn.click(
679
+ fn=_load_starter,
680
+ inputs=[starter_choice],
681
+ outputs=[user_input],
682
+ )
683
+
684
+ return demo
685
+
686
+
687
+ if __name__ == "__main__":
688
+ demo = build_interface()
689
+ demo.launch()