ZENLLC commited on
Commit
3150bee
·
verified ·
1 Parent(s): 99089d3

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +51 -47
app.py CHANGED
@@ -10,7 +10,7 @@ from google.genai import types
10
  from PIL import Image
11
 
12
  # -------------------------------------------------------------------
13
- # Config
14
  # -------------------------------------------------------------------
15
 
16
  APP_TITLE = "ZEN AI Co. Module 2 | Agent Assembler"
@@ -20,12 +20,11 @@ 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
- # Reasonable defaults if user doesn't touch sliders
24
  DEFAULT_TEMPERATURE = 0.6
25
  DEFAULT_MAX_TOKENS = 1024
26
 
27
  # -------------------------------------------------------------------
28
- # Helpers: API clients
29
  # -------------------------------------------------------------------
30
 
31
  def get_openai_client(key_override: Optional[str] = None) -> OpenAI:
@@ -33,14 +32,12 @@ def get_openai_client(key_override: Optional[str] = None) -> OpenAI:
33
  Returns an OpenAI client using either:
34
  1) key from the UI override, or
35
  2) OPENAI_API_KEY environment variable.
36
-
37
- This satisfies the “two places for API keys” requirement.
38
  """
39
  api_key = (key_override or "").strip() or os.getenv("OPENAI_API_KEY", "").strip()
40
  if not api_key:
41
  raise ValueError(
42
  "OpenAI API key missing. "
43
- "Either set OPENAI_API_KEY env var or paste it in the sidebar."
44
  )
45
  return OpenAI(api_key=api_key)
46
 
@@ -55,12 +52,12 @@ def get_google_client(key_override: Optional[str] = None) -> genai.Client:
55
  if not api_key:
56
  raise ValueError(
57
  "Google Gemini API key missing. "
58
- "Either set GOOGLE_API_KEY env var or paste it in the sidebar."
59
  )
60
  return genai.Client(api_key=api_key)
61
 
62
  # -------------------------------------------------------------------
63
- # Helpers: Prompt & style shaping
64
  # -------------------------------------------------------------------
65
 
66
  def build_system_instructions(
@@ -70,21 +67,27 @@ def build_system_instructions(
70
  tone: str,
71
  ) -> str:
72
  """
73
- Builds a strong system prompt that shapes behavior according to theme,
74
- output mode, and tone.
75
  """
76
  theme_map = {
77
  "ZEN Dark": "Use a sleek, modern, slightly futuristic tone. Be concise but high signal.",
78
  "ZEN Light": "Use a clear, friendly, educational tone suitable for learners of all ages.",
79
  "Research / Technical": "Write like a senior research engineer: rigorous, structured, and explicit.",
80
- "Youth AI Pioneer": "Explain things in simple, motivating language suitable for ages 11–18, "
81
- "but never dumb it down.",
 
 
82
  }
83
 
84
  output_map = {
85
  "Standard Chat": "Respond like a normal assistant, but keep paragraphs tight and skimmable.",
86
- "Executive Report": "Respond as a structured executive brief with headings, bullets, and 1–2 sentence insights.",
87
- "Infographic Outline": "Respond as a bullet-point infographic blueprint with short, punchy lines and clear sections.",
 
 
 
 
 
88
  "Bullet Summary": "Respond as a compact bullet summary with 5–10 bullets max.",
89
  }
90
 
@@ -112,7 +115,7 @@ def history_to_messages(
112
  system_instructions: str,
113
  ) -> List[dict]:
114
  """
115
- Converts Gradio Chatbot history into OpenAI-style messages.
116
  """
117
  messages: List[dict] = []
118
  if system_instructions:
@@ -134,7 +137,7 @@ def history_to_gemini_prompt(
134
  system_instructions: str,
135
  ) -> str:
136
  """
137
- Flattens history into a single text prompt for Gemini.
138
  """
139
  lines = []
140
  if system_instructions:
@@ -151,7 +154,7 @@ def history_to_gemini_prompt(
151
  return "\n\n".join(lines)
152
 
153
  # -------------------------------------------------------------------
154
- # Helpers: Model calls (text)
155
  # -------------------------------------------------------------------
156
 
157
  def call_openai_text(
@@ -162,7 +165,7 @@ def call_openai_text(
162
  ) -> str:
163
  client = get_openai_client(openai_key)
164
  completion = client.chat.completions.create(
165
- model="gpt-5", # You can change to gpt-5.1 or whatever is available in your project
166
  messages=messages,
167
  temperature=temperature,
168
  max_tokens=max_tokens,
@@ -199,7 +202,7 @@ def call_hybrid_text(
199
  max_tokens: int,
200
  ) -> str:
201
  """
202
- Calls GPT-5 and Gemini (2.5 Pro or 3 Pro) and fuses their answers.
203
  """
204
  try:
205
  gpt_answer = call_openai_text(openai_key, messages, temperature, max_tokens)
@@ -222,7 +225,7 @@ def call_hybrid_text(
222
  return fused
223
 
224
  # -------------------------------------------------------------------
225
- # Helpers: Image generation
226
  # -------------------------------------------------------------------
227
 
228
  def call_openai_dalle(
@@ -231,7 +234,7 @@ def call_openai_dalle(
231
  size: str = "1024x1024",
232
  ) -> Optional[Image.Image]:
233
  """
234
- Uses DALL·E 3 via OpenAI Images API to generate a PIL image.
235
  """
236
  client = get_openai_client(openai_key)
237
  response = client.images.generate(
@@ -243,7 +246,6 @@ def call_openai_dalle(
243
  if not response.data:
244
  return None
245
 
246
- # DALL·E responses can be URL or base64; here we handle base64
247
  img_data = response.data[0].b64_json
248
  img_bytes = base64.b64decode(img_data)
249
  return Image.open(BytesIO(img_bytes))
@@ -255,7 +257,7 @@ def call_gemini_image(
255
  prompt: str,
256
  ) -> Optional[Image.Image]:
257
  """
258
- Uses Nano Banana (gemini-2.5-flash-image) or Nano Banana Pro
259
  (gemini-3-pro-image-preview) via Google GenAI SDK.
260
  """
261
  client = get_google_client(google_key)
@@ -264,7 +266,7 @@ def call_gemini_image(
264
  contents=[prompt],
265
  )
266
 
267
- # Follow pattern from official docs: walk parts for inline image data
268
  for candidate in response.candidates:
269
  for part in candidate.content.parts:
270
  inline = getattr(part, "inline_data", None)
@@ -275,7 +277,7 @@ def call_gemini_image(
275
  return None
276
 
277
  # -------------------------------------------------------------------
278
- # Core chat function used by Gradio
279
  # -------------------------------------------------------------------
280
 
281
  def agent_assembler_chat(
@@ -294,13 +296,13 @@ def agent_assembler_chat(
294
  image_backend: str,
295
  ) -> Tuple[List[Tuple[str, str]], Optional[Image.Image]]:
296
  """
297
- Main callback for the app. Returns updated chat history & optional image.
298
  """
299
  if not user_message.strip():
300
  return chat_history, None
301
 
302
  base_system = (
303
- "You are ZEN AI Co.'s **Agent Assembler**, a multi-model orchestrator. "
304
  "You can:\n"
305
  "- Hold deep, contextual conversations about AI literacy, automation, and education.\n"
306
  "- Generate executive reports and structured briefs.\n"
@@ -320,11 +322,11 @@ def agent_assembler_chat(
320
  tone=tone,
321
  )
322
 
323
- # Prepare conversations for both stacks
324
  messages = history_to_messages(chat_history, user_message, system_instructions)
325
  gemini_prompt = history_to_gemini_prompt(chat_history, user_message, system_instructions)
326
 
327
- # Decide which text model(s) to call
328
  if model_family == "OpenAI: GPT-5":
329
  ai_reply = call_openai_text(
330
  openai_key=openai_key_ui,
@@ -345,7 +347,7 @@ def agent_assembler_chat(
345
  temperature=temperature,
346
  max_tokens=max_tokens,
347
  )
348
- else: # Hybrid mode
349
  if gemini_model_choice == "Gemini 2.5 Pro":
350
  model_id = "gemini-2.5-pro"
351
  else:
@@ -361,18 +363,17 @@ def agent_assembler_chat(
361
  max_tokens=max_tokens,
362
  )
363
 
364
- # Update chat history
365
  chat_history = chat_history + [(user_message, ai_reply)]
366
 
367
  # Optional image generation
368
  generated_image: Optional[Image.Image] = None
369
  if generate_image:
370
- # Build an image-oriented prompt from the last user query + output mode
371
  image_prompt = (
372
  f"{user_message.strip()}\n\n"
373
  f"Image intent: {output_mode}. "
374
- "Render clean, readable text if any labels are required. "
375
- "Use a style that would fit the ZEN AI Co. brand."
376
  )
377
 
378
  try:
@@ -393,7 +394,7 @@ def agent_assembler_chat(
393
  prompt=image_prompt,
394
  )
395
  except Exception as e:
396
- # Append a note to the assistant message if image fails
397
  chat_history[-1] = (
398
  chat_history[-1][0],
399
  chat_history[-1][1]
@@ -403,7 +404,7 @@ def agent_assembler_chat(
403
  return chat_history, generated_image
404
 
405
 
406
- def clear_chat():
407
  return [], None
408
 
409
  # -------------------------------------------------------------------
@@ -416,11 +417,10 @@ def build_interface() -> gr.Blocks:
416
  gr.Markdown(APP_DESCRIPTION)
417
 
418
  with gr.Row():
419
- # Left: Chat + image output
420
  with gr.Column(scale=3):
421
  chatbot = gr.Chatbot(
422
  label="Agent Assembler Chat",
423
- type="messages",
424
  height=520,
425
  )
426
  image_out = gr.Image(
@@ -430,7 +430,9 @@ def build_interface() -> gr.Blocks:
430
  )
431
  user_input = gr.Textbox(
432
  label="Your message",
433
- placeholder="Ask for a chat, a report, an infographic outline, or an image...",
 
 
434
  lines=3,
435
  )
436
 
@@ -438,15 +440,15 @@ def build_interface() -> gr.Blocks:
438
  send_btn = gr.Button("Send", variant="primary")
439
  clear_btn = gr.Button("Clear")
440
 
441
- # Right: Control panel
442
  with gr.Column(scale=2):
443
  gr.Markdown("## API Keys")
444
  openai_key_ui = gr.Textbox(
445
- label="OpenAI API Key (optional, otherwise uses OPENAI_API_KEY env var)",
446
  type="password",
447
  )
448
  google_key_ui = gr.Textbox(
449
- label="Google Gemini API Key (optional, otherwise uses GOOGLE_API_KEY env var)",
450
  type="password",
451
  )
452
 
@@ -496,7 +498,7 @@ def build_interface() -> gr.Blocks:
496
  value="Neutral",
497
  )
498
 
499
- gr.Markdown("## Generation Controls")
500
 
501
  temperature = gr.Slider(
502
  label="Temperature (creativity)",
@@ -531,10 +533,10 @@ def build_interface() -> gr.Blocks:
531
  value="Nano Banana Pro (Gemini 3 Pro Image Preview)",
532
  )
533
 
534
- # State for chat history
535
  chat_state = gr.State([])
536
 
537
- # Wire up events
538
  send_btn.click(
539
  fn=agent_assembler_chat,
540
  inputs=[
@@ -554,11 +556,12 @@ def build_interface() -> gr.Blocks:
554
  ],
555
  outputs=[chatbot, image_out],
556
  ).then(
557
- fn=lambda h: (h, ""), # update state + clear box
558
  inputs=chatbot,
559
  outputs=[chat_state, user_input],
560
  )
561
 
 
562
  user_input.submit(
563
  fn=agent_assembler_chat,
564
  inputs=[
@@ -578,11 +581,12 @@ def build_interface() -> gr.Blocks:
578
  ],
579
  outputs=[chatbot, image_out],
580
  ).then(
581
- fn=lambda h: (h, ""), # update state + clear box
582
  inputs=chatbot,
583
  outputs=[chat_state, user_input],
584
  )
585
 
 
586
  clear_btn.click(
587
  fn=clear_chat,
588
  inputs=None,
 
10
  from PIL import Image
11
 
12
  # -------------------------------------------------------------------
13
+ # App Metadata
14
  # -------------------------------------------------------------------
15
 
16
  APP_TITLE = "ZEN AI Co. Module 2 | Agent Assembler"
 
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:
 
32
  Returns an OpenAI client using either:
33
  1) key from the UI override, or
34
  2) OPENAI_API_KEY environment variable.
 
 
35
  """
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
 
 
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(
 
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
 
 
115
  system_instructions: str,
116
  ) -> List[dict]:
117
  """
118
+ Convert chat history into OpenAI chat messages.
119
  """
120
  messages: List[dict] = []
121
  if system_instructions:
 
137
  system_instructions: str,
138
  ) -> str:
139
  """
140
+ Flatten history into a single text prompt for Gemini.
141
  """
142
  lines = []
143
  if system_instructions:
 
154
  return "\n\n".join(lines)
155
 
156
  # -------------------------------------------------------------------
157
+ # Text Model Calls
158
  # -------------------------------------------------------------------
159
 
160
  def call_openai_text(
 
165
  ) -> str:
166
  client = get_openai_client(openai_key)
167
  completion = client.chat.completions.create(
168
+ model="gpt-5", # adjust to the exact GPT-5 variant available to you
169
  messages=messages,
170
  temperature=temperature,
171
  max_tokens=max_tokens,
 
202
  max_tokens: int,
203
  ) -> str:
204
  """
205
+ Call GPT-5 and a Gemini model, then fuse the outputs.
206
  """
207
  try:
208
  gpt_answer = call_openai_text(openai_key, messages, temperature, max_tokens)
 
225
  return fused
226
 
227
  # -------------------------------------------------------------------
228
+ # Image Generation Calls
229
  # -------------------------------------------------------------------
230
 
231
  def call_openai_dalle(
 
234
  size: str = "1024x1024",
235
  ) -> Optional[Image.Image]:
236
  """
237
+ Use DALL·E 3 to generate a PIL image.
238
  """
239
  client = get_openai_client(openai_key)
240
  response = client.images.generate(
 
246
  if not response.data:
247
  return None
248
 
 
249
  img_data = response.data[0].b64_json
250
  img_bytes = base64.b64decode(img_data)
251
  return Image.open(BytesIO(img_bytes))
 
257
  prompt: str,
258
  ) -> Optional[Image.Image]:
259
  """
260
+ Use Nano Banana (gemini-2.5-flash-image) or Nano Banana Pro
261
  (gemini-3-pro-image-preview) via Google GenAI SDK.
262
  """
263
  client = get_google_client(google_key)
 
266
  contents=[prompt],
267
  )
268
 
269
+ # Walk candidate parts for inline image data
270
  for candidate in response.candidates:
271
  for part in candidate.content.parts:
272
  inline = getattr(part, "inline_data", None)
 
277
  return None
278
 
279
  # -------------------------------------------------------------------
280
+ # Core Chat Logic
281
  # -------------------------------------------------------------------
282
 
283
  def agent_assembler_chat(
 
296
  image_backend: str,
297
  ) -> Tuple[List[Tuple[str, str]], Optional[Image.Image]]:
298
  """
299
+ Main callback: text + optional image generation.
300
  """
301
  if not user_message.strip():
302
  return chat_history, None
303
 
304
  base_system = (
305
+ "You are ZEN AI Co.'s Agent Assembler, a multi-model orchestrator. "
306
  "You can:\n"
307
  "- Hold deep, contextual conversations about AI literacy, automation, and education.\n"
308
  "- Generate executive reports and structured briefs.\n"
 
322
  tone=tone,
323
  )
324
 
325
+ # Prepare inputs for OpenAI and Gemini
326
  messages = history_to_messages(chat_history, user_message, system_instructions)
327
  gemini_prompt = history_to_gemini_prompt(chat_history, user_message, system_instructions)
328
 
329
+ # Route text generation
330
  if model_family == "OpenAI: GPT-5":
331
  ai_reply = call_openai_text(
332
  openai_key=openai_key_ui,
 
347
  temperature=temperature,
348
  max_tokens=max_tokens,
349
  )
350
+ else: # Hybrid: GPT-5 + Gemini
351
  if gemini_model_choice == "Gemini 2.5 Pro":
352
  model_id = "gemini-2.5-pro"
353
  else:
 
363
  max_tokens=max_tokens,
364
  )
365
 
366
+ # Update history with text
367
  chat_history = chat_history + [(user_message, ai_reply)]
368
 
369
  # Optional image generation
370
  generated_image: Optional[Image.Image] = None
371
  if generate_image:
 
372
  image_prompt = (
373
  f"{user_message.strip()}\n\n"
374
  f"Image intent: {output_mode}. "
375
+ "Render clean, readable text if labels are required. "
376
+ "Use a visual style aligned with the ZEN AI Co. brand."
377
  )
378
 
379
  try:
 
394
  prompt=image_prompt,
395
  )
396
  except Exception as e:
397
+ # Attach error note to latest assistant message
398
  chat_history[-1] = (
399
  chat_history[-1][0],
400
  chat_history[-1][1]
 
404
  return chat_history, generated_image
405
 
406
 
407
+ def clear_chat() -> Tuple[List[Tuple[str, str]], Optional[Image.Image]]:
408
  return [], None
409
 
410
  # -------------------------------------------------------------------
 
417
  gr.Markdown(APP_DESCRIPTION)
418
 
419
  with gr.Row():
420
+ # Left side: chat + image output
421
  with gr.Column(scale=3):
422
  chatbot = gr.Chatbot(
423
  label="Agent Assembler Chat",
 
424
  height=520,
425
  )
426
  image_out = gr.Image(
 
430
  )
431
  user_input = gr.Textbox(
432
  label="Your message",
433
+ placeholder=(
434
+ "Ask for a chat, a report, an infographic outline, or an image..."
435
+ ),
436
  lines=3,
437
  )
438
 
 
440
  send_btn = gr.Button("Send", variant="primary")
441
  clear_btn = gr.Button("Clear")
442
 
443
+ # Right side: control panel
444
  with gr.Column(scale=2):
445
  gr.Markdown("## API Keys")
446
  openai_key_ui = gr.Textbox(
447
+ label="OpenAI API Key (optional; otherwise uses OPENAI_API_KEY env var)",
448
  type="password",
449
  )
450
  google_key_ui = gr.Textbox(
451
+ label="Google Gemini API Key (optional; otherwise uses GOOGLE_API_KEY env var)",
452
  type="password",
453
  )
454
 
 
498
  value="Neutral",
499
  )
500
 
501
+ gr.Markdown("## Text Generation Controls")
502
 
503
  temperature = gr.Slider(
504
  label="Temperature (creativity)",
 
533
  value="Nano Banana Pro (Gemini 3 Pro Image Preview)",
534
  )
535
 
536
+ # Shared chat state
537
  chat_state = gr.State([])
538
 
539
+ # Send button wiring
540
  send_btn.click(
541
  fn=agent_assembler_chat,
542
  inputs=[
 
556
  ],
557
  outputs=[chatbot, image_out],
558
  ).then(
559
+ fn=lambda h: (h, ""), # sync state, clear input
560
  inputs=chatbot,
561
  outputs=[chat_state, user_input],
562
  )
563
 
564
+ # Submit on Enter
565
  user_input.submit(
566
  fn=agent_assembler_chat,
567
  inputs=[
 
581
  ],
582
  outputs=[chatbot, image_out],
583
  ).then(
584
+ fn=lambda h: (h, ""),
585
  inputs=chatbot,
586
  outputs=[chat_state, user_input],
587
  )
588
 
589
+ # Clear button wiring
590
  clear_btn.click(
591
  fn=clear_chat,
592
  inputs=None,