ZENLLC commited on
Commit
9ef12ca
·
verified ·
1 Parent(s): c544a3b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +254 -303
app.py CHANGED
@@ -5,8 +5,6 @@ from typing import List, Dict, Optional, Tuple
5
 
6
  import gradio as gr
7
  from openai import OpenAI
8
- from google import genai
9
- from google.genai import types
10
  from PIL import Image
11
 
12
  # -------------------------------------------------------------------
@@ -15,16 +13,23 @@ from PIL import Image
15
 
16
  APP_TITLE = "ZEN AI Co. Module 2 | Agent Assembler"
17
  APP_DESCRIPTION = """
18
- Multi-model agent that can chat, draft reports, generate infographic briefs,
19
- and create images using GPT-5, Gemini 2.5 Pro, Gemini 3 Pro, Nano Banana,
20
- Nano Banana Pro, and DALL·E 3.
 
 
 
 
21
  """
22
 
23
- DEFAULT_TEMPERATURE = 0.6
 
24
  DEFAULT_MAX_TOKENS = 1024
 
 
25
 
26
  # -------------------------------------------------------------------
27
- # API Clients
28
  # -------------------------------------------------------------------
29
 
30
  def get_openai_client(key_override: Optional[str] = None) -> OpenAI:
@@ -36,75 +41,61 @@ def get_openai_client(key_override: Optional[str] = None) -> OpenAI:
36
  api_key = (key_override or "").strip() or os.getenv("OPENAI_API_KEY", "").strip()
37
  if not api_key:
38
  raise ValueError(
39
- "OpenAI API key missing. "
40
- "Set OPENAI_API_KEY env var or paste it into the sidebar."
41
  )
42
  return OpenAI(api_key=api_key)
43
 
44
-
45
- def get_google_client(key_override: Optional[str] = None) -> genai.Client:
46
- """
47
- Returns a Google GenAI client using either:
48
- 1) key from the UI override, or
49
- 2) GOOGLE_API_KEY environment variable.
50
- """
51
- api_key = (key_override or "").strip() or os.getenv("GOOGLE_API_KEY", "").strip()
52
- if not api_key:
53
- raise ValueError(
54
- "Google Gemini API key missing. "
55
- "Set GOOGLE_API_KEY env var or paste it into the sidebar."
56
- )
57
- return genai.Client(api_key=api_key)
58
-
59
  # -------------------------------------------------------------------
60
  # Prompt & Style Helpers
61
  # -------------------------------------------------------------------
62
 
63
  def build_system_instructions(
64
- base_instructions: str,
65
- theme: str,
66
  output_mode: str,
67
  tone: str,
68
  ) -> str:
69
  """
70
- Builds a system prompt that reflects theme, output mode, and tone.
 
71
  """
72
- theme_map = {
73
- "ZEN Dark": "Use a sleek, modern, slightly futuristic tone. Be concise but high signal.",
74
- "ZEN Light": "Use a clear, friendly, educational tone suitable for learners of all ages.",
75
- "Research / Technical": "Write like a senior research engineer: rigorous, structured, and explicit.",
76
- "Youth AI Pioneer": (
77
- "Explain things in simple, motivating language for ages 11–18, "
78
- "but keep the concepts accurate and serious."
79
- ),
80
  }
81
 
82
  output_map = {
83
- "Standard Chat": "Respond like a normal assistant, but keep paragraphs tight and skimmable.",
84
  "Executive Report": (
85
- "Respond as a structured executive brief with headings, bullets, "
86
- "and 1–2 sentence insights per section."
87
  ),
88
  "Infographic Outline": (
89
- "Respond as a bullet-point infographic blueprint with short, punchy lines and clear sections."
 
 
 
 
90
  ),
91
- "Bullet Summary": "Respond as a compact bullet summary with 5–10 bullets max.",
92
  }
93
 
94
  tone_map = {
95
- "Neutral": "Keep style neutral and globally understandable.",
96
- "Bold / Visionary": "Lean into visionary, high-energy language while staying precise and concrete.",
97
- "Minimalist": "Be extremely concise; prioritize clarity over flourish.",
98
  }
99
 
100
  parts = [
101
- base_instructions.strip(),
102
  "",
103
- f"STYLE THEME: {theme_map.get(theme, '')}",
104
  f"OUTPUT MODE: {output_map.get(output_mode, '')}",
105
  f"TONE: {tone_map.get(tone, '')}",
106
  "",
107
- "Always format results cleanly in Markdown.",
108
  ]
109
  return "\n".join(p for p in parts if p.strip())
110
 
@@ -115,123 +106,53 @@ def history_to_openai_messages(
115
  system_instructions: str,
116
  ) -> List[Dict[str, str]]:
117
  """
118
- Convert Chatbot-style history (list of {role, content}) into an
119
- OpenAI messages list with an added system message and new user query.
120
  """
121
  messages: List[Dict[str, str]] = []
122
  if system_instructions:
123
  messages.append({"role": "system", "content": system_instructions})
124
 
125
- # Reuse history directly
126
- for msg in history_messages:
127
- if msg.get("role") in ("user", "assistant", "system"):
128
- messages.append({"role": msg["role"], "content": msg["content"]})
129
-
130
- messages.append({"role": "user", "content": user_message})
131
- return messages
132
-
133
-
134
- def history_to_gemini_prompt_from_messages(
135
- history_messages: List[Dict[str, str]],
136
- user_message: str,
137
- system_instructions: str,
138
- ) -> str:
139
- """
140
- Flatten Chatbot-style message history into a single prompt for Gemini.
141
- """
142
- lines: List[str] = []
143
- if system_instructions:
144
- lines.append(f"SYSTEM:\n{system_instructions}\n")
145
-
146
  for msg in history_messages:
147
  role = msg.get("role")
148
  content = msg.get("content", "")
149
- if role == "user":
150
- lines.append(f"USER: {content}")
151
- elif role == "assistant":
152
- lines.append(f"ASSISTANT: {content}")
153
- elif role == "system":
154
- lines.append(f"SYSTEM (prior): {content}")
155
 
156
- lines.append(f"USER: {user_message}")
157
- lines.append("ASSISTANT:")
158
- return "\n\n".join(lines)
159
 
160
  # -------------------------------------------------------------------
161
- # Text Model Calls
162
  # -------------------------------------------------------------------
163
 
164
  def call_openai_text(
165
  openai_key: Optional[str],
166
  messages: List[Dict[str, str]],
167
  temperature: float,
 
168
  max_tokens: int,
 
 
169
  ) -> str:
 
 
 
170
  client = get_openai_client(openai_key)
171
  completion = client.chat.completions.create(
172
- model="gpt-5", # adjust to actual GPT-5 variant in your project if needed
173
  messages=messages,
174
  temperature=temperature,
175
- max_tokens=max_tokens,
 
 
 
176
  )
177
  return completion.choices[0].message.content
178
 
179
 
180
- def call_gemini_text(
181
- google_key: Optional[str],
182
- model_id: str,
183
- prompt: str,
184
- temperature: float,
185
- max_tokens: int,
186
- ) -> str:
187
- client = get_google_client(google_key)
188
- response = client.models.generate_content(
189
- model=model_id,
190
- contents=[prompt],
191
- config=types.GenerateContentConfig(
192
- temperature=temperature,
193
- max_output_tokens=max_tokens,
194
- ),
195
- )
196
- return response.text
197
-
198
-
199
- def call_hybrid_text(
200
- openai_key: Optional[str],
201
- google_key: Optional[str],
202
- gemini_model_id: str,
203
- messages: List[Dict[str, str]],
204
- gemini_prompt: str,
205
- temperature: float,
206
- max_tokens: int,
207
- ) -> str:
208
- """
209
- Call GPT-5 and a Gemini model, then fuse the outputs.
210
- """
211
- try:
212
- gpt_answer = call_openai_text(openai_key, messages, temperature, max_tokens)
213
- except Exception as e:
214
- gpt_answer = f"[GPT-5 call failed: {e}]"
215
-
216
- try:
217
- gemini_answer = call_gemini_text(
218
- google_key, gemini_model_id, gemini_prompt, temperature, max_tokens
219
- )
220
- except Exception as e:
221
- gemini_answer = f"[Gemini call failed: {e}]"
222
-
223
- fused = (
224
- "### GPT-5 Perspective\n"
225
- f"{gpt_answer}\n\n"
226
- "### Gemini Perspective\n"
227
- f"{gemini_answer}"
228
- )
229
- return fused
230
-
231
- # -------------------------------------------------------------------
232
- # Image Generation Calls
233
- # -------------------------------------------------------------------
234
-
235
  def call_openai_dalle(
236
  openai_key: Optional[str],
237
  prompt: str,
@@ -254,34 +175,36 @@ def call_openai_dalle(
254
  img_bytes = base64.b64decode(img_data)
255
  return Image.open(BytesIO(img_bytes))
256
 
 
 
 
257
 
258
- def call_gemini_image(
259
- google_key: Optional[str],
260
- model_id: str,
261
- prompt: str,
262
- ) -> Optional[Image.Image]:
263
- """
264
- Use Nano Banana (gemini-2.5-flash-image) or Nano Banana Pro
265
- (gemini-3-pro-image-preview) via Google GenAI SDK.
266
- """
267
- client = get_google_client(google_key)
268
- response = client.models.generate_content(
269
- model=model_id,
270
- contents=[prompt],
271
- )
272
 
273
- # Walk candidate parts for inline image data
274
- for candidate in getattr(response, "candidates", []):
275
- content = getattr(candidate, "content", None)
276
- if not content:
277
- continue
278
- for part in getattr(content, "parts", []):
279
- inline = getattr(part, "inline_data", None)
280
- if inline and getattr(inline, "data", None):
281
- img_bytes = base64.b64decode(inline.data)
282
- return Image.open(BytesIO(img_bytes))
283
 
284
- return None
 
 
 
 
 
 
 
 
 
 
 
285
 
286
  # -------------------------------------------------------------------
287
  # Core Chat Logic
@@ -291,98 +214,61 @@ def agent_assembler_chat(
291
  user_message: str,
292
  chat_history: List[Dict[str, str]],
293
  openai_key_ui: str,
294
- google_key_ui: str,
295
- model_family: str,
296
- gemini_model_choice: str,
297
  output_mode: str,
298
- theme: str,
299
  tone: str,
300
  temperature: float,
 
301
  max_tokens: int,
 
 
302
  generate_image: bool,
303
- image_backend: str,
 
304
  ) -> Tuple[List[Dict[str, str]], Optional[Image.Image]]:
305
  """
306
- Main callback: text + optional image generation.
307
-
308
- chat_history is a list of messages:
309
- [{"role": "user", "content": "..."}, {"role": "assistant", "content": "..."}]
310
- which matches the Gradio Chatbot in messages mode.
311
  """
312
  if not user_message.strip():
313
  return chat_history, None
314
 
315
- base_system = (
316
- "You are ZEN AI Co.'s Agent Assembler, a multi-model orchestrator. "
317
- "You can:\n"
318
- "- Hold deep, contextual conversations about AI literacy, automation, and education.\n"
319
- "- Generate executive reports and structured briefs.\n"
320
- "- Produce detailed infographic blueprints with clear sections and labels.\n"
321
- "- Collaborate with image models by designing precise, typo-free prompts.\n"
322
- "\n"
323
- "Always:\n"
324
- "- Avoid hallucinating APIs or capabilities you don't actually have.\n"
325
- "- Make outputs copy-paste-ready for real projects.\n"
326
- "- Keep spelling and formatting extremely precise."
327
- )
328
-
329
  system_instructions = build_system_instructions(
330
- base_instructions=base_system,
331
- theme=theme,
332
  output_mode=output_mode,
333
  tone=tone,
334
  )
335
 
336
- # Prepare messages for OpenAI and Gemini
337
  openai_messages = history_to_openai_messages(
338
  history_messages=chat_history,
339
  user_message=user_message,
340
  system_instructions=system_instructions,
341
  )
342
- gemini_prompt = history_to_gemini_prompt_from_messages(
343
- history_messages=chat_history,
344
- user_message=user_message,
345
- system_instructions=system_instructions,
346
- )
347
 
348
- # Route text generation
349
- if model_family == "OpenAI: GPT-5":
350
  ai_reply = call_openai_text(
351
  openai_key=openai_key_ui,
352
  messages=openai_messages,
353
  temperature=temperature,
 
354
  max_tokens=max_tokens,
 
 
355
  )
356
- elif model_family.startswith("Google Gemini"):
357
- if gemini_model_choice == "Gemini 2.5 Pro":
358
- model_id = "gemini-2.5-pro"
359
- else:
360
- model_id = "gemini-3-pro-preview"
361
-
362
- ai_reply = call_gemini_text(
363
- google_key=google_key_ui,
364
- model_id=model_id,
365
- prompt=gemini_prompt,
366
- temperature=temperature,
367
- max_tokens=max_tokens,
368
- )
369
- else: # Hybrid: GPT-5 + Gemini
370
- if gemini_model_choice == "Gemini 2.5 Pro":
371
- model_id = "gemini-2.5-pro"
372
- else:
373
- model_id = "gemini-3-pro-preview"
374
-
375
- ai_reply = call_hybrid_text(
376
- openai_key=openai_key_ui,
377
- google_key=google_key_ui,
378
- gemini_model_id=model_id,
379
- messages=openai_messages,
380
- gemini_prompt=gemini_prompt,
381
- temperature=temperature,
382
- max_tokens=max_tokens,
383
  )
384
 
385
- # Update history with new user + assistant messages
386
  chat_history = chat_history + [
387
  {"role": "user", "content": user_message},
388
  {"role": "assistant", "content": ai_reply},
@@ -391,34 +277,34 @@ def agent_assembler_chat(
391
  # Optional image generation
392
  generated_image: Optional[Image.Image] = None
393
  if generate_image:
 
 
 
 
 
 
 
 
 
394
  image_prompt = (
395
  f"{user_message.strip()}\n\n"
396
- f"Image intent: {output_mode}. "
397
- "Render clean, readable text if labels are required. "
398
- "Use a visual style aligned with the ZEN AI Co. brand."
399
  )
400
 
401
  try:
402
- if image_backend == "DALL·E 3 (OpenAI)":
403
- generated_image = call_openai_dalle(
404
- openai_key=openai_key_ui, prompt=image_prompt
405
- )
406
- elif image_backend == "Nano Banana (Gemini 2.5 Flash Image)":
407
- generated_image = call_gemini_image(
408
- google_key=google_key_ui,
409
- model_id="gemini-2.5-flash-image",
410
- prompt=image_prompt,
411
- )
412
- else: # Nano Banana Pro
413
- generated_image = call_gemini_image(
414
- google_key=google_key_ui,
415
- model_id="gemini-3-pro-image-preview",
416
- prompt=image_prompt,
417
- )
418
  except Exception as e:
419
- # Attach error note to the latest assistant message
420
  if chat_history and chat_history[-1].get("role") == "assistant":
421
- chat_history[-1]["content"] += f"\n\n_Image generation failed: {e}_"
 
 
 
422
 
423
  return chat_history, generated_image
424
 
@@ -433,67 +319,89 @@ def clear_chat() -> Tuple[List[Dict[str, str]], Optional[Image.Image]]:
433
  # Gradio UI
434
  # -------------------------------------------------------------------
435
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
436
  def build_interface() -> gr.Blocks:
437
  with gr.Blocks(title=APP_TITLE) as demo:
438
  gr.Markdown(f"# {APP_TITLE}")
439
  gr.Markdown(APP_DESCRIPTION)
440
 
441
  with gr.Row():
442
- # Left side: chat + image output
443
  with gr.Column(scale=3):
444
  chatbot = gr.Chatbot(
445
  label="Agent Assembler Chat",
446
  height=520,
447
  )
448
  image_out = gr.Image(
449
- label="Latest Generated Image",
450
  height=320,
451
  interactive=False,
452
  )
 
453
  user_input = gr.Textbox(
454
  label="Your message",
455
- placeholder=(
456
- "Ask for a chat, a report, an infographic outline, or an image..."
457
- ),
458
  lines=3,
459
  )
460
 
461
  with gr.Row():
462
  send_btn = gr.Button("Send", variant="primary")
463
- clear_btn = gr.Button("Clear")
 
 
 
 
 
 
 
 
464
 
465
- # Right side: control panel
466
  with gr.Column(scale=2):
467
- gr.Markdown("## API Keys")
 
468
  openai_key_ui = gr.Textbox(
469
  label="OpenAI API Key (optional; otherwise uses OPENAI_API_KEY env var)",
470
  type="password",
471
  )
472
- google_key_ui = gr.Textbox(
473
- label="Google Gemini API Key (optional; otherwise uses GOOGLE_API_KEY env var)",
474
- type="password",
 
 
475
  )
476
 
477
- gr.Markdown("## Model & Style")
478
 
479
- model_family = gr.Radio(
480
- label="Primary Model Routing",
481
  choices=[
482
- "OpenAI: GPT-5",
483
- "Google Gemini: Single",
484
- "Hybrid: GPT-5 + Gemini",
 
485
  ],
486
- value="Hybrid: GPT-5 + Gemini",
487
- )
488
-
489
- gemini_model_choice = gr.Radio(
490
- label="Gemini Model",
491
- choices=["Gemini 2.5 Pro", "Gemini 3 Pro (preview)"],
492
- value="Gemini 3 Pro (preview)",
493
  )
494
 
495
  output_mode = gr.Radio(
496
- label="Output Mode",
497
  choices=[
498
  "Standard Chat",
499
  "Executive Report",
@@ -503,78 +411,109 @@ def build_interface() -> gr.Blocks:
503
  value="Standard Chat",
504
  )
505
 
506
- theme = gr.Radio(
507
- label="Theme (response style)",
508
- choices=[
509
- "ZEN Dark",
510
- "ZEN Light",
511
- "Research / Technical",
512
- "Youth AI Pioneer",
513
- ],
514
- value="ZEN Dark",
515
- )
516
-
517
  tone = gr.Radio(
518
  label="Tone",
519
- choices=["Neutral", "Bold / Visionary", "Minimalist"],
 
 
 
 
520
  value="Neutral",
521
  )
522
 
523
- gr.Markdown("## Text Generation Controls")
524
 
525
  temperature = gr.Slider(
526
- label="Temperature (creativity)",
527
  minimum=0.0,
528
  maximum=1.5,
529
  value=DEFAULT_TEMPERATURE,
530
  step=0.05,
531
  )
532
 
 
 
 
 
 
 
 
 
533
  max_tokens = gr.Slider(
534
- label="Max Tokens (text length)",
535
  minimum=128,
536
  maximum=4096,
537
  value=DEFAULT_MAX_TOKENS,
538
  step=128,
539
  )
540
 
541
- gr.Markdown("## Image Generation")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
542
 
543
  generate_image = gr.Checkbox(
544
  label="Also generate an image for this message",
545
  value=False,
546
  )
547
 
548
- image_backend = gr.Radio(
549
- label="Image Backend",
550
  choices=[
551
- "DALL·E 3 (OpenAI)",
552
- "Nano Banana (Gemini 2.5 Flash Image)",
553
- "Nano Banana Pro (Gemini 3 Pro Image Preview)",
 
 
554
  ],
555
- value="Nano Banana Pro (Gemini 3 Pro Image Preview)",
 
 
 
 
 
 
 
 
 
 
556
  )
557
 
558
  # Shared chat state: list of messages (dicts)
559
  chat_state = gr.State([])
560
 
561
- # Send button wiring
562
  send_btn.click(
563
  fn=agent_assembler_chat,
564
  inputs=[
565
  user_input,
566
  chat_state,
567
  openai_key_ui,
568
- google_key_ui,
569
- model_family,
570
- gemini_model_choice,
571
  output_mode,
572
- theme,
573
  tone,
574
  temperature,
 
575
  max_tokens,
 
 
576
  generate_image,
577
- image_backend,
 
578
  ],
579
  outputs=[chatbot, image_out],
580
  ).then(
@@ -590,16 +529,18 @@ def build_interface() -> gr.Blocks:
590
  user_input,
591
  chat_state,
592
  openai_key_ui,
593
- google_key_ui,
594
- model_family,
595
- gemini_model_choice,
596
  output_mode,
597
- theme,
598
  tone,
599
  temperature,
 
600
  max_tokens,
 
 
601
  generate_image,
602
- image_backend,
 
603
  ],
604
  outputs=[chatbot, image_out],
605
  ).then(
@@ -608,7 +549,7 @@ def build_interface() -> gr.Blocks:
608
  outputs=[chat_state, user_input],
609
  )
610
 
611
- # Clear button wiring
612
  clear_btn.click(
613
  fn=clear_chat,
614
  inputs=None,
@@ -619,6 +560,16 @@ def build_interface() -> gr.Blocks:
619
  outputs=chat_state,
620
  )
621
 
 
 
 
 
 
 
 
 
 
 
622
  return demo
623
 
624
 
 
5
 
6
  import gradio as gr
7
  from openai import OpenAI
 
 
8
  from PIL import Image
9
 
10
  # -------------------------------------------------------------------
 
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 for image generation.
20
+ • Lets you edit the system prompt, role, tone, and output format.
21
+ • Provides sliders and controls to experiment with sampling and behavior.
22
+ • Includes starter prompts to show different use cases (chat, reports, infographics, visuals).
23
  """
24
 
25
+ DEFAULT_TEMPERATURE = 0.7
26
+ DEFAULT_TOP_P = 1.0
27
  DEFAULT_MAX_TOKENS = 1024
28
+ DEFAULT_PRESENCE_PENALTY = 0.0
29
+ DEFAULT_FREQUENCY_PENALTY = 0.0
30
 
31
  # -------------------------------------------------------------------
32
+ # OpenAI Client Helper
33
  # -------------------------------------------------------------------
34
 
35
  def get_openai_client(key_override: Optional[str] = None) -> OpenAI:
 
41
  api_key = (key_override or "").strip() or os.getenv("OPENAI_API_KEY", "").strip()
42
  if not api_key:
43
  raise ValueError(
44
+ "OpenAI API key missing. Set OPENAI_API_KEY env var "
45
+ "or paste it into the sidebar."
46
  )
47
  return OpenAI(api_key=api_key)
48
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
  # -------------------------------------------------------------------
50
  # Prompt & Style Helpers
51
  # -------------------------------------------------------------------
52
 
53
  def build_system_instructions(
54
+ user_system_prompt: str,
55
+ assistant_role: str,
56
  output_mode: str,
57
  tone: str,
58
  ) -> str:
59
  """
60
+ Build a system prompt string combining user-provided base instructions
61
+ with role + format + tone metadata.
62
  """
63
+ role_map = {
64
+ "General Assistant": "Behave as a highly capable, calm general-purpose AI assistant.",
65
+ "Teacher / Instructor": "Behave as a patient educator. Explain concepts step-by-step and check for understanding.",
66
+ "Engineer / Architect": "Behave as a senior engineer and systems architect. Be explicit, structured, and precise.",
67
+ "Storyteller / Creative": "Behave as a creative storyteller. Use vivid but clear language while staying coherent.",
 
 
 
68
  }
69
 
70
  output_map = {
71
+ "Standard Chat": "Respond like a normal chat, with paragraphs kept short and skimmable.",
72
  "Executive Report": (
73
+ "Respond as an executive brief with headings, short sections, and bullet points "
74
+ "highlighting key decisions and risks."
75
  ),
76
  "Infographic Outline": (
77
+ "Respond as an infographic blueprint with section titles and short bullet lines. "
78
+ "Focus on clarity and label-friendly phrases."
79
+ ),
80
+ "Bullet Summary": (
81
+ "Respond as a tight bullet summary (5–10 bullets) capturing only the most important details."
82
  ),
 
83
  }
84
 
85
  tone_map = {
86
+ "Neutral": "Keep the tone neutral and globally understandable.",
87
+ "Bold / Visionary": "Use confident, forward-looking language, but stay concrete and honest.",
88
+ "Minimalist": "Be extremely concise. Prefer fewer words and high information density.",
89
  }
90
 
91
  parts = [
92
+ (user_system_prompt or "").strip(),
93
  "",
94
+ f"ASSISTANT ROLE: {role_map.get(assistant_role, '')}",
95
  f"OUTPUT MODE: {output_map.get(output_mode, '')}",
96
  f"TONE: {tone_map.get(tone, '')}",
97
  "",
98
+ "Always output clean Markdown.",
99
  ]
100
  return "\n".join(p for p in parts if p.strip())
101
 
 
106
  system_instructions: str,
107
  ) -> List[Dict[str, str]]:
108
  """
109
+ Convert Chatbot-style history (list of {role, content}) into an OpenAI
110
+ messages list with an added system message and new user query.
111
  """
112
  messages: List[Dict[str, str]] = []
113
  if system_instructions:
114
  messages.append({"role": "system", "content": system_instructions})
115
 
116
+ # Reuse existing history
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
  for msg in history_messages:
118
  role = msg.get("role")
119
  content = msg.get("content", "")
120
+ if role in ("user", "assistant", "system") and content:
121
+ messages.append({"role": role, "content": content})
 
 
 
 
122
 
123
+ # New query
124
+ messages.append({"role": "user", "content": user_message})
125
+ return messages
126
 
127
  # -------------------------------------------------------------------
128
+ # OpenAI Text & Image Calls
129
  # -------------------------------------------------------------------
130
 
131
  def call_openai_text(
132
  openai_key: Optional[str],
133
  messages: List[Dict[str, str]],
134
  temperature: float,
135
+ top_p: float,
136
  max_tokens: int,
137
+ presence_penalty: float,
138
+ frequency_penalty: float,
139
  ) -> str:
140
+ """
141
+ Call GPT-5 via Chat Completions using the new max_completion_tokens parameter.
142
+ """
143
  client = get_openai_client(openai_key)
144
  completion = client.chat.completions.create(
145
+ model="gpt-5", # change to exact variant you have (e.g. "gpt-5.1-mini") if needed
146
  messages=messages,
147
  temperature=temperature,
148
+ top_p=top_p,
149
+ presence_penalty=presence_penalty,
150
+ frequency_penalty=frequency_penalty,
151
+ max_completion_tokens=max_tokens,
152
  )
153
  return completion.choices[0].message.content
154
 
155
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
156
  def call_openai_dalle(
157
  openai_key: Optional[str],
158
  prompt: str,
 
175
  img_bytes = base64.b64decode(img_data)
176
  return Image.open(BytesIO(img_bytes))
177
 
178
+ # -------------------------------------------------------------------
179
+ # Starter Prompts
180
+ # -------------------------------------------------------------------
181
 
182
+ STARTER_PROMPTS = {
183
+ "Explain AI Literacy to a 13-year-old":
184
+ "Explain what AI literacy is to a 13-year-old who loves YouTube and video games. "
185
+ "Use examples from their world and end with 3 practical things they can do this week.",
186
+
187
+ "Executive Brief: AI Strategy for a Nonprofit":
188
+ "Create an executive brief for a youth-serving nonprofit that wants to adopt AI tools. "
189
+ "Include priorities, risks, and quick wins in under 800 words.",
 
 
 
 
 
 
190
 
191
+ "Infographic Outline: ZEN AI Pioneer Program":
192
+ "Create an infographic outline that explains the ZEN AI Pioneer Program: "
193
+ "what it is, who it serves, what makes it historic, and 3 key stats. "
194
+ "Make the sections short and label-friendly.",
 
 
 
 
 
 
195
 
196
+ "Creative Image Prompt: Futuristic ZEN AI Lab":
197
+ "Describe a futuristic but realistic ZEN AI Co. lab where youth are building their own AI tools. "
198
+ "Focus on what the scene looks like so it can be turned into an illustration. "
199
+ "End with a separate final paragraph that is ONLY the pure image prompt text.",
200
+
201
+ "Debugging Prompt: Why is my model hallucinating?":
202
+ "I built a small AI app and the model is hallucinating facts about my organization. "
203
+ "Explain why that happens and propose a 3-layer mitigation strategy (prompting, retrieval, UX).",
204
+ }
205
+
206
+ def get_starter_prompt(choice: str) -> str:
207
+ return STARTER_PROMPTS.get(choice, "")
208
 
209
  # -------------------------------------------------------------------
210
  # Core Chat Logic
 
214
  user_message: str,
215
  chat_history: List[Dict[str, str]],
216
  openai_key_ui: str,
217
+ system_prompt_ui: str,
218
+ assistant_role: str,
 
219
  output_mode: str,
 
220
  tone: str,
221
  temperature: float,
222
+ top_p: float,
223
  max_tokens: int,
224
+ presence_penalty: float,
225
+ frequency_penalty: float,
226
  generate_image: bool,
227
+ image_style: str,
228
+ image_aspect: str,
229
  ) -> Tuple[List[Dict[str, str]], Optional[Image.Image]]:
230
  """
231
+ Main callback: GPT-5 text + optional DALL·E 3 image.
232
+ chat_history is a list of messages: [{role, content}, ...]
 
 
 
233
  """
234
  if not user_message.strip():
235
  return chat_history, None
236
 
237
+ # Build system instructions
 
 
 
 
 
 
 
 
 
 
 
 
 
238
  system_instructions = build_system_instructions(
239
+ user_system_prompt=system_prompt_ui,
240
+ assistant_role=assistant_role,
241
  output_mode=output_mode,
242
  tone=tone,
243
  )
244
 
245
+ # Prepare messages for OpenAI
246
  openai_messages = history_to_openai_messages(
247
  history_messages=chat_history,
248
  user_message=user_message,
249
  system_instructions=system_instructions,
250
  )
 
 
 
 
 
251
 
252
+ # Call GPT-5
253
+ try:
254
  ai_reply = call_openai_text(
255
  openai_key=openai_key_ui,
256
  messages=openai_messages,
257
  temperature=temperature,
258
+ top_p=top_p,
259
  max_tokens=max_tokens,
260
+ presence_penalty=presence_penalty,
261
+ frequency_penalty=frequency_penalty,
262
  )
263
+ except Exception as e:
264
+ ai_reply = (
265
+ "There was an error calling GPT-5.\n\n"
266
+ f"Short message: `{e}`\n\n"
267
+ "Check that your API key is valid and that the model name matches "
268
+ "what is available in your OpenAI account."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
269
  )
270
 
271
+ # Update history
272
  chat_history = chat_history + [
273
  {"role": "user", "content": user_message},
274
  {"role": "assistant", "content": ai_reply},
 
277
  # Optional image generation
278
  generated_image: Optional[Image.Image] = None
279
  if generate_image:
280
+ # Map aspect label to DALL·E size
281
+ aspect_to_size = {
282
+ "Square (1:1)": "1024x1024",
283
+ "Portrait (9:16)": "1024x1792",
284
+ "Landscape (16:9)": "1792x1024",
285
+ }
286
+ size = aspect_to_size.get(image_aspect, "1024x1024")
287
+
288
+ # Build image prompt
289
  image_prompt = (
290
  f"{user_message.strip()}\n\n"
291
+ f"IMAGE STYLE: {image_style}. "
292
+ "High readability, clean composition, suitable for presentations or infographics."
 
293
  )
294
 
295
  try:
296
+ generated_image = call_openai_dalle(
297
+ openai_key=openai_key_ui,
298
+ prompt=image_prompt,
299
+ size=size,
300
+ )
 
 
 
 
 
 
 
 
 
 
 
301
  except Exception as e:
302
+ # Attach error note to latest assistant message
303
  if chat_history and chat_history[-1].get("role") == "assistant":
304
+ chat_history[-1]["content"] += (
305
+ f"\n\n_Image generation failed: `{e}`. "
306
+ "Check your OpenAI key and dalle-3 availability._"
307
+ )
308
 
309
  return chat_history, generated_image
310
 
 
319
  # Gradio UI
320
  # -------------------------------------------------------------------
321
 
322
+ DEFAULT_SYSTEM_PROMPT = """You are ZEN AI Co.'s Agent Assembler.
323
+
324
+ You help people understand how large language models and image models work
325
+ by giving clear, practical, and honest answers. You are allowed to:
326
+
327
+ - Explain how prompts, system messages, and parameters change behavior.
328
+ - Suggest better prompts and show before/after improvements.
329
+ - Design prompts for text and images that are copy-paste-ready.
330
+ - Produce reports, outlines, and infographic blueprints.
331
+
332
+ You must:
333
+
334
+ - Avoid making up API capabilities that do not exist.
335
+ - Be honest when you don't know something or lack context.
336
+ - Keep spelling and formatting very precise, especially in prompts and labels.
337
+ """
338
+
339
  def build_interface() -> gr.Blocks:
340
  with gr.Blocks(title=APP_TITLE) as demo:
341
  gr.Markdown(f"# {APP_TITLE}")
342
  gr.Markdown(APP_DESCRIPTION)
343
 
344
  with gr.Row():
345
+ # LEFT: Chat + image + input
346
  with gr.Column(scale=3):
347
  chatbot = gr.Chatbot(
348
  label="Agent Assembler Chat",
349
  height=520,
350
  )
351
  image_out = gr.Image(
352
+ label="Latest Generated Image (DALL·E 3)",
353
  height=320,
354
  interactive=False,
355
  )
356
+
357
  user_input = gr.Textbox(
358
  label="Your message",
359
+ placeholder="Ask a question, request a report, or describe a scene for image generation...",
 
 
360
  lines=3,
361
  )
362
 
363
  with gr.Row():
364
  send_btn = gr.Button("Send", variant="primary")
365
+ clear_btn = gr.Button("Clear Chat")
366
+
367
+ gr.Markdown("### Starter Prompts")
368
+ starter_choice = gr.Dropdown(
369
+ label="Pick a starter prompt to auto-fill the input",
370
+ choices=list(STARTER_PROMPTS.keys()),
371
+ value="Explain AI Literacy to a 13-year-old",
372
+ )
373
+ starter_btn = gr.Button("Load Starter Prompt")
374
 
375
+ # RIGHT: Controls / teaching panel
376
  with gr.Column(scale=2):
377
+ gr.Markdown("## API & System Prompt")
378
+
379
  openai_key_ui = gr.Textbox(
380
  label="OpenAI API Key (optional; otherwise uses OPENAI_API_KEY env var)",
381
  type="password",
382
  )
383
+
384
+ system_prompt_ui = gr.Textbox(
385
+ label="System / Model Instructions",
386
+ value=DEFAULT_SYSTEM_PROMPT,
387
+ lines=10,
388
  )
389
 
390
+ gr.Markdown("## Behavior & Style")
391
 
392
+ assistant_role = gr.Radio(
393
+ label="Assistant Role",
394
  choices=[
395
+ "General Assistant",
396
+ "Teacher / Instructor",
397
+ "Engineer / Architect",
398
+ "Storyteller / Creative",
399
  ],
400
+ value="General Assistant",
 
 
 
 
 
 
401
  )
402
 
403
  output_mode = gr.Radio(
404
+ label="Output Format",
405
  choices=[
406
  "Standard Chat",
407
  "Executive Report",
 
411
  value="Standard Chat",
412
  )
413
 
 
 
 
 
 
 
 
 
 
 
 
414
  tone = gr.Radio(
415
  label="Tone",
416
+ choices=[
417
+ "Neutral",
418
+ "Bold / Visionary",
419
+ "Minimalist",
420
+ ],
421
  value="Neutral",
422
  )
423
 
424
+ gr.Markdown("## Sampling (Experiment Zone)")
425
 
426
  temperature = gr.Slider(
427
+ label="Temperature (creativity / randomness)",
428
  minimum=0.0,
429
  maximum=1.5,
430
  value=DEFAULT_TEMPERATURE,
431
  step=0.05,
432
  )
433
 
434
+ top_p = gr.Slider(
435
+ label="Top-p (nucleus sampling)",
436
+ minimum=0.1,
437
+ maximum=1.0,
438
+ value=DEFAULT_TOP_P,
439
+ step=0.05,
440
+ )
441
+
442
  max_tokens = gr.Slider(
443
+ label="Max completion tokens",
444
  minimum=128,
445
  maximum=4096,
446
  value=DEFAULT_MAX_TOKENS,
447
  step=128,
448
  )
449
 
450
+ presence_penalty = gr.Slider(
451
+ label="Presence penalty (encourage new topics)",
452
+ minimum=-2.0,
453
+ maximum=2.0,
454
+ value=DEFAULT_PRESENCE_PENALTY,
455
+ step=0.1,
456
+ )
457
+
458
+ frequency_penalty = gr.Slider(
459
+ label="Frequency penalty (discourage repetition)",
460
+ minimum=-2.0,
461
+ maximum=2.0,
462
+ value=DEFAULT_FREQUENCY_PENALTY,
463
+ step=0.1,
464
+ )
465
+
466
+ gr.Markdown("## Image Generation (DALL·E 3)")
467
 
468
  generate_image = gr.Checkbox(
469
  label="Also generate an image for this message",
470
  value=False,
471
  )
472
 
473
+ image_style = gr.Radio(
474
+ label="Image Style",
475
  choices=[
476
+ "Futuristic glass UI dashboard",
477
+ "Clean infographic illustration",
478
+ "Soft watercolor concept art",
479
+ "High-contrast comic / graphic novel",
480
+ "Photorealistic lab / studio scene",
481
  ],
482
+ value="Clean infographic illustration",
483
+ )
484
+
485
+ image_aspect = gr.Radio(
486
+ label="Aspect Ratio",
487
+ choices=[
488
+ "Square (1:1)",
489
+ "Portrait (9:16)",
490
+ "Landscape (16:9)",
491
+ ],
492
+ value="Square (1:1)",
493
  )
494
 
495
  # Shared chat state: list of messages (dicts)
496
  chat_state = gr.State([])
497
 
498
+ # Send button: main call
499
  send_btn.click(
500
  fn=agent_assembler_chat,
501
  inputs=[
502
  user_input,
503
  chat_state,
504
  openai_key_ui,
505
+ system_prompt_ui,
506
+ assistant_role,
 
507
  output_mode,
 
508
  tone,
509
  temperature,
510
+ top_p,
511
  max_tokens,
512
+ presence_penalty,
513
+ frequency_penalty,
514
  generate_image,
515
+ image_style,
516
+ image_aspect,
517
  ],
518
  outputs=[chatbot, image_out],
519
  ).then(
 
529
  user_input,
530
  chat_state,
531
  openai_key_ui,
532
+ system_prompt_ui,
533
+ assistant_role,
 
534
  output_mode,
 
535
  tone,
536
  temperature,
537
+ top_p,
538
  max_tokens,
539
+ presence_penalty,
540
+ frequency_penalty,
541
  generate_image,
542
+ image_style,
543
+ image_aspect,
544
  ],
545
  outputs=[chatbot, image_out],
546
  ).then(
 
549
  outputs=[chat_state, user_input],
550
  )
551
 
552
+ # Clear chat
553
  clear_btn.click(
554
  fn=clear_chat,
555
  inputs=None,
 
560
  outputs=chat_state,
561
  )
562
 
563
+ # Starter prompt loader
564
+ def _load_starter(choice: str) -> str:
565
+ return get_starter_prompt(choice)
566
+
567
+ starter_btn.click(
568
+ fn=_load_starter,
569
+ inputs=[starter_choice],
570
+ outputs=[user_input],
571
+ )
572
+
573
  return demo
574
 
575