rairo commited on
Commit
37739f9
Β·
verified Β·
1 Parent(s): 951bcdd

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +214 -269
app.py CHANGED
@@ -1,30 +1,23 @@
1
  ###############################################################################
2
- # app.py – AI Report & Slides Demo (stable 2025-06-23)
3
  ###############################################################################
4
  import os, re, json, uuid, tempfile, asyncio, base64, io, hashlib
5
  from pathlib import Path
6
-
7
- import streamlit as st
8
- import pandas as pd
9
- import matplotlib
10
- matplotlib.use("Agg")
11
  import matplotlib.pyplot as plt
12
-
13
  from fpdf import FPDF, HTMLMixin
14
  from markdown import markdown
15
- try:
16
- from markdown_it import MarkdownIt
17
- except ImportError:
18
- MarkdownIt = None
19
-
20
  from pptx import Presentation
21
  from pptx.util import Inches, Pt
22
 
23
- # ── Google APIs ──────────────────────────────────────────────────────────────
24
  from google.oauth2.service_account import Credentials
25
  from googleapiclient.discovery import build
26
 
27
- # ── Gemini & ADK ─────────────────────────────────────────────────────────────
28
  from google import genai
29
  from google.genai import types
30
  from google.adk.agents import LlmAgent, SequentialAgent
@@ -33,283 +26,235 @@ from google.adk.sessions import InMemorySessionService
33
  from langchain_experimental.agents import create_pandas_dataframe_agent
34
  from langchain_google_genai import ChatGoogleGenerativeAI
35
 
36
- ###############################################################################
37
- # 1. Constants & env
38
- ###############################################################################
39
- FONT_DIR = Path(__file__).parent
40
- FONT_NAME = "NotoSans"
41
- FONT_REG = FONT_DIR / "NotoSans-Regular.ttf"
42
- FONT_BOLD = FONT_DIR / "NotoSans-Bold.ttf"
43
-
44
- SLIDES = 7
45
- TTS_MODEL = "gemini-2.5-flash-preview-tts"
46
- CREDS_JSON = os.getenv("GOOGLE_SERVICE_ACCOUNT_JSON") # optional
47
 
48
  os.environ["STREAMLIT_CONFIG_DIR"] = tempfile.gettempdir()
49
  os.environ["MPLCONFIGDIR"] = tempfile.gettempdir()
50
 
51
- ###############################################################################
52
- # 2. Utility helpers (all cached where heavy)
53
- ###############################################################################
54
- def _hash_df(df: pd.DataFrame) -> str:
 
 
 
 
 
 
55
  return hashlib.sha1(pd.util.hash_pandas_object(df, index=True).values).hexdigest()
56
 
 
 
 
 
 
 
 
 
 
 
57
  @st.cache_resource(show_spinner=False)
58
- def get_pandas_agent(df_hash: str, api_key: str):
59
  return create_pandas_dataframe_agent(
60
- llm=ChatGoogleGenerativeAI(model="gemini-2.0-flash",
61
- google_api_key=api_key),
62
  df=st.session_state.df,
63
- verbose=False,
64
- allow_dangerous_code=True)
65
-
66
- def fix_bullets(text: str) -> str:
67
- """Convert stray Win-1252 bullet (0x95) β†’ proper Unicode bullet."""
68
- return text.replace("\x95", "β€’")
69
-
70
- def generate_tts_audio(client, text):
71
- text = (text[:500] + "...") if len(text) > 500 else text
72
- try:
73
- resp = client.models.generate_content(
74
- model=TTS_MODEL,
75
- contents=f"Say clearly: {text}",
76
- config=types.GenerateContentConfig(
77
- response_modalities=["AUDIO"],
78
- speech_config=types.SpeechConfig(
79
- voice_config=types.VoiceConfig(
80
- prebuilt_voice_config=types.PrebuiltVoiceConfig(
81
- voice_name="Kore")))))
82
- audio = resp.candidates[0].content.parts[0].inline_data
83
- return audio.data, audio.mime_type
84
- except Exception as e:
85
- st.error(f"TTS error: {e}")
86
- return None, None
87
-
88
- def pcm_to_wav(raw: bytes, rate=24000, width=2):
89
- import wave, contextlib
90
- buf = io.BytesIO()
91
- with contextlib.closing(wave.open(buf, "wb")) as wf:
92
- wf.setnchannels(1); wf.setsampwidth(width); wf.setframerate(rate); wf.writeframes(raw)
93
- return buf.getvalue()
94
 
95
  @st.cache_resource(show_spinner=False)
96
- def build_charts(chart_descs: tuple, df_hash: str, api_key: str):
97
- """Return {desc:str β†’ path:str} for unique chart descriptions."""
98
- agent = get_pandas_agent(df_hash, api_key)
99
- desc2path = {}
100
- for desc in chart_descs:
101
- if desc in desc2path: continue
102
- agent.run(f"Create a {desc} using matplotlib")
103
- fig = plt.gcf()
104
- p = Path(tempfile.gettempdir()) / f"{uuid.uuid4()}.png"
105
- fig.savefig(p, dpi=300, bbox_inches="tight"); plt.close()
106
- desc2path[desc] = str(p)
107
- return desc2path
108
 
109
  @st.cache_resource(show_spinner=False)
110
- def build_pdf(markdown_src: str) -> bytes:
111
- """Return final PDF bytes with fonts, charts, tables fixed."""
112
- markdown_src = fix_bullets(markdown_src)
113
- # 1 Markdown β†’ HTML
114
- html = (MarkdownIt("commonmark", {"breaks": True}).enable("table").render(markdown_src)
115
- if MarkdownIt else
116
- markdown(markdown_src, extensions=["tables"]))
117
- # 2 Dummy pdf for metrics
118
- tmp = FPDF(); w_mm, px = tmp.w - 2*tmp.l_margin, lambda mm: int(mm*tmp.k)
119
- # 3 Tables
120
- html = re.sub(r'(<table[^>]*>)', r'\1<font size="8">', html)\
121
- .replace('</table>', '</font></table>')
122
- html = re.sub(r'<table(?![^>]*\bwidth=)', lambda m: f'<table width="{px(w_mm)}"', html)
123
- # 4 Scale imgs
124
- html = re.sub(r'<img\s+src="([^"]+\.png)"\s*/?>',
125
- lambda m: f'<img src="{m.group(1)}" width="{int(w_mm)}" />', html)
126
- # 5 PDF build
127
- class PDF(FPDF, HTMLMixin): pass
128
- pdf = PDF(); pdf.set_auto_page_break(auto=True, margin=15)
129
- for style, path in [("",FONT_REG), ("B",FONT_BOLD), ("I",FONT_REG), ("BI",FONT_BOLD)]:
130
- pdf.add_font(FONT_NAME, style, str(path), uni=True)
131
- pdf.set_fallback_fonts([FONT_NAME])
132
- pdf.add_page(); pdf.set_font(FONT_NAME, "B", 18)
133
- pdf.cell(0, 12, "AI-Generated Business Report", ln=True); pdf.ln(4)
134
- pdf.set_font(FONT_NAME, "", 11); pdf.write_html(html)
135
  return bytes(pdf.output(dest="S"))
136
 
137
  @st.cache_resource(show_spinner=False)
138
- def build_pptx(slide_texts: tuple, desc2path: dict) -> Path:
139
- prs = Presentation(); blank = prs.slide_layouts[6]
140
- for i, txt in enumerate(slide_texts):
141
- txt = fix_bullets(txt)
142
- slide = prs.slides.add_slide(blank)
143
- slide.shapes.add_textbox(Inches(0.6), Inches(0.4), Inches(9), Inches(0.8))\
144
- .text_frame.text = f"Slide {i+1}"
145
- body = slide.shapes.add_textbox(Inches(0.6), Inches(1.4), Inches(9), Inches(3.2))
146
- body.text_frame.text = re.sub(r'<.*?>', '', txt)
147
- m = re.search(r'<generate_chart:\s*"([^"]+)"\s*>', txt)
148
- if m and m.group(1) in desc2path:
149
- slide.shapes.add_picture(desc2path[m.group(1)],
150
- Inches(1), Inches(3.7), width=Inches(8))
151
- out = Path(tempfile.gettempdir()) / f"slides_{uuid.uuid4().hex}.pptx"
152
- prs.save(out); return out
 
153
 
154
- def upload_to_slides(pptx_path: Path) -> str | None:
155
- if not CREDS_JSON:
156
- st.warning("Service-account JSON not configured; skipping Slides upload.")
157
- return None
158
- creds = Credentials.from_service_account_file(
159
  CREDS_JSON,
160
  scopes=["https://www.googleapis.com/auth/drive.file",
161
  "https://www.googleapis.com/auth/presentations"])
162
- drive = build("drive", "v3", credentials=creds)
163
- meta = {"name": f"AI-Slides-{uuid.uuid4().hex[:6]}",
164
- "mimeType": "application/vnd.google-apps.presentation"}
165
- media = {"mimeType": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
166
- "body": open(pptx_path, "rb")}
167
- f = drive.files().create(body=meta, media_body=media).execute()
168
  return f"https://docs.google.com/presentation/d/{f['id']}"
169
 
170
- ###############################################################################
171
- # 3. Streamlit UI
172
- ###############################################################################
173
- st.set_page_config(page_title="AI Report & Slides", layout="wide")
174
- st.title("πŸ“Š AI-Generated Report & Presentation")
175
-
176
- API_KEY = os.getenv("GEMINI_API_KEY")
177
- if not API_KEY:
178
- st.error("Set `GEMINI_API_KEY` in env."); st.stop()
179
- client = genai.Client(api_key=API_KEY)
180
-
181
- mode = st.radio("Output:", ["Report", "Presentation", "Both"], horizontal=True)
182
- uploaded = st.file_uploader("Upload CSV / XLSX", ["csv", "xlsx"])
183
- user_ctx = st.text_area("Optional business context / objectives")
184
- run_btn = st.button("πŸš€ Generate")
185
-
186
- ###############################################################################
187
- # 4. Main pipeline
188
- ###############################################################################
189
- if run_btn:
190
- if not uploaded: st.warning("Please upload a dataset."); st.stop()
191
- # 4.1 Load
192
- try:
193
- df = pd.read_excel(uploaded) if uploaded.name.lower().endswith(".xlsx") else pd.read_csv(uploaded)
194
- except Exception as e:
195
- st.error(f"Read error: {e}"); st.stop()
196
- st.session_state.df = df
197
- df_hash = _hash_df(df)
198
-
199
- # 4.2 Agents
200
- report_agent = LlmAgent(
201
- name="ReportAgent", model="gemini-2.5-flash",
202
- instruction=("You are a senior business analyst. Write an executive-level Markdown "
203
- "report with <generate_chart: \"...\"> placeholders."))
204
-
205
- pres_agent = LlmAgent(
206
- name="PresentationAgent", model="gemini-2.5-flash",
207
- instruction=(f"Create exactly {SLIDES} slides. Each slide starts with 'Slide X:' "
208
- "and may include <generate_chart: \"...\"> tags."))
209
-
210
- subs = []
211
- if mode in ("Report", "Both"): subs.append(report_agent)
212
- if mode in ("Presentation", "Both"): subs.append(pres_agent)
213
- pipeline = SequentialAgent(name="Pipeline", sub_agents=subs)
214
-
215
- # 4.3 Context β†’ Gemini
216
- ctx = {"dataset_info": {"shape": df.shape,
217
- "columns": list(df.columns),
218
- "dtypes": df.dtypes.astype(str).to_dict(),
219
- "missing": df.isna().sum().to_dict()},
220
- "user_context": user_ctx,
221
- "preview": df.head().to_dict()}
222
-
223
- # 4.4 Run ADK
224
- svc, sid = InMemorySessionService(), str(uuid.uuid4())
225
- asyncio.run(svc.create_session(app_name="app", user_id="user", session_id=sid))
226
- runner = Runner(agent=pipeline, app_name="app", session_service=svc)
227
-
228
- final = {}
229
- async def _collect():
230
- async for ev in runner.run_async(
231
- user_id="user", session_id=sid,
232
- new_message=types.Content(role="user",
233
- parts=[types.Part(text=json.dumps(ctx))])):
234
- if ev.is_final_response():
235
- final[ev.author] = ev.content.parts[0].text
236
- asyncio.run(_collect())
237
-
238
- # 4.5 Charts
239
- chart_tags = re.findall(r'<generate_chart:\s*"([^"]+)"\s*>', "\n".join(final.values()))
240
- desc2path = build_charts(tuple(chart_tags), df_hash, API_KEY)
241
-
242
- # 4.6 Tabs
243
- if mode == "Both":
244
- tab_report, tab_pres = st.tabs(["πŸ“„ Report", "πŸ“‘ Presentation"])
245
- else:
246
- tab_report = tab_pres = st
247
-
248
- #######################################################################
249
- # Report
250
- #######################################################################
251
- if mode in ("Report", "Both"):
252
- md = fix_bullets(final["ReportAgent"])
253
- # Replace tags: base-64 for preview + file path for PDF
254
- preview_md = md; pdf_md = md
255
- for d,pth in desc2path.items():
256
- with open(pth, "rb") as f:
257
- b64 = base64.b64encode(f.read()).decode()
258
- embed = f'<img src="data:image/png;base64,{b64}" style="max-width:100%;margin:8px 0;" />'
259
- preview_md = re.sub(rf'<generate_chart:\s*"{re.escape(d)}"\s*>', embed, preview_md)
260
- pdf_md = re.sub(rf'<generate_chart:\s*"{re.escape(d)}"\s*>',
261
- f'<img src="{pth}" />', pdf_md)
262
-
263
- with tab_report:
264
- st.markdown("### Report Preview")
265
- st.markdown(preview_md, unsafe_allow_html=True)
266
- st.download_button("⬇️ Download PDF",
267
- build_pdf(pdf_md),
268
- "business_report.pdf",
269
- mime="application/pdf")
270
-
271
- #######################################################################
272
- # Presentation
273
- #######################################################################
274
- if mode in ("Presentation", "Both"):
275
- raw = fix_bullets(final["PresentationAgent"])
276
- slides_txt = [s.strip() for s in re.split(r'\bSlide\s+\d+:', raw) if s.strip()][:SLIDES]
277
- st.session_state.slides = slides_txt
278
- st.session_state.curr = 0
279
-
280
- def render(idx: int):
281
- txt = st.session_state.slides[idx]
282
- st.markdown(f"#### Slide {idx+1} / {len(slides_txt)}")
283
- st.write(txt)
284
- if st.button("πŸ”Š Narrate", key=f"tts_{idx}"):
285
- with st.spinner("TTS…"):
286
- aud,mime = generate_tts_audio(client, txt)
287
- if aud:
288
- if 'pcm' in mime or 'L16' in mime:
289
- st.audio(pcm_to_wav(aud), format="audio/wav")
290
- else:
291
- st.audio(aud, format=mime)
292
-
293
- with tab_pres:
294
- col1,col2 = st.columns(2)
295
- if col1.button("⬅️ Back", disabled=st.session_state.curr==0):
296
- st.session_state.curr -= 1
297
- if col2.button("Next ➑️",
298
- disabled=st.session_state.curr==len(slides_txt)-1):
299
- st.session_state.curr += 1
300
- render(st.session_state.curr)
301
-
302
- # PPTX build (cached)
303
- pptx = build_pptx(tuple(slides_txt), desc2path)
304
- st.download_button("⬇️ Download PPTX",
305
- pptx.read_bytes(),
306
- "presentation.pptx",
307
- mime=("application/vnd.openxmlformats-officedocument."
308
- "presentationml.presentation"))
309
- if st.button("⬆️ Upload to Google Slides"):
310
- url = upload_to_slides(pptx)
311
- if url:
312
- st.success(f"[Open Slides]({url})")
313
  ###############################################################################
314
  # End
315
  ###############################################################################
 
1
  ###############################################################################
2
+ # app.py – streamlined & stable
3
  ###############################################################################
4
  import os, re, json, uuid, tempfile, asyncio, base64, io, hashlib
5
  from pathlib import Path
6
+ import streamlit as st; st.set_page_config("AI Report & Slides", layout="wide")
7
+ import pandas as pd, matplotlib; matplotlib.use("Agg")
 
 
 
8
  import matplotlib.pyplot as plt
 
9
  from fpdf import FPDF, HTMLMixin
10
  from markdown import markdown
11
+ try: from markdown_it import MarkdownIt
12
+ except ImportError: MarkdownIt = None
 
 
 
13
  from pptx import Presentation
14
  from pptx.util import Inches, Pt
15
 
16
+ # Google APIs
17
  from google.oauth2.service_account import Credentials
18
  from googleapiclient.discovery import build
19
 
20
+ # Gemini / ADK
21
  from google import genai
22
  from google.genai import types
23
  from google.adk.agents import LlmAgent, SequentialAgent
 
26
  from langchain_experimental.agents import create_pandas_dataframe_agent
27
  from langchain_google_genai import ChatGoogleGenerativeAI
28
 
29
+ # ─────────────────────────────────────────────────────────────────────────────
30
+ # CONFIG
31
+ # ─────────────────────────────────────────────────────────────────────────────
32
+ FONT_DIR = Path(__file__).parent
33
+ FONT_REG = FONT_DIR / "NotoSans-Regular.ttf"
34
+ FONT_BLD = FONT_DIR / "NotoSans-Bold.ttf"
35
+ FONT_NAME = "NotoSans"
36
+ SLIDE_COUNT = 7
37
+ TTS_MODEL = "gemini-2.5-flash-preview-tts"
38
+ CREDS_JSON = os.getenv("GOOGLE_SERVICE_ACCOUNT_JSON") # optional
 
39
 
40
  os.environ["STREAMLIT_CONFIG_DIR"] = tempfile.gettempdir()
41
  os.environ["MPLCONFIGDIR"] = tempfile.gettempdir()
42
 
43
+ API_KEY = os.getenv("GEMINI_API_KEY")
44
+ if not API_KEY: st.error("Set GEMINI_API_KEY"); st.stop()
45
+ GEMINI = genai.Client(api_key=API_KEY)
46
+
47
+ # ─────────────────────────────────────────────────────────────────────────────
48
+ # SMALL HELPERS
49
+ # ─────────────────────────────────────────────────────────────────────────────
50
+ fix_bullets = lambda txt: txt.replace("\x95", "β€’")
51
+
52
+ def hash_df(df: pd.DataFrame) -> str:
53
  return hashlib.sha1(pd.util.hash_pandas_object(df, index=True).values).hexdigest()
54
 
55
+ def pcm_to_wav(raw: bytes, rate=24_000):
56
+ import wave, contextlib, io as _io
57
+ buf=_io.BytesIO()
58
+ with contextlib.closing(wave.open(buf, "wb")) as f:
59
+ f.setnchannels(1); f.setsampwidth(2); f.setframerate(rate); f.writeframes(raw)
60
+ return buf.getvalue()
61
+
62
+ # ─────────────────────────────────────────────────────────────────────────────
63
+ # CACHED BUILDERS
64
+ # ─────────────────────────────────────────────────────────────────────────────
65
  @st.cache_resource(show_spinner=False)
66
+ def pandas_agent(df_hash: str):
67
  return create_pandas_dataframe_agent(
68
+ llm=ChatGoogleGenerativeAI(model="gemini-2.0-flash", google_api_key=API_KEY),
 
69
  df=st.session_state.df,
70
+ verbose=False, allow_dangerous_code=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
 
72
  @st.cache_resource(show_spinner=False)
73
+ def build_charts(descs: tuple, df_hash: str):
74
+ agent = pandas_agent(df_hash)
75
+ out={}
76
+ for d in descs:
77
+ if d in out: continue
78
+ agent.run(f"Create a {d} using matplotlib"); fig=plt.gcf()
79
+ p=Path(tempfile.gettempdir())/f"{uuid.uuid4()}.png"
80
+ fig.savefig(p,dpi=300,bbox_inches="tight"); plt.close()
81
+ out[d]=str(p)
82
+ return out
 
 
83
 
84
  @st.cache_resource(show_spinner=False)
85
+ def build_pdf(html_ready_md: str) -> bytes:
86
+ html_ready_md = fix_bullets(html_ready_md)
87
+ html = (MarkdownIt("commonmark",{"breaks":True}).enable("table").render(html_ready_md)
88
+ if MarkdownIt else markdown(html_ready_md,extensions=["tables"]))
89
+ dummy=FPDF(); page_w=dummy.w-2*dummy.l_margin; px=lambda mm:int(mm*dummy.k)
90
+ html=re.sub(r'(<table[^>]*>)',r'\1<font size="8">',html).replace('</table>','</font></table>')
91
+ html=re.sub(r'<table(?![^>]*width=)',lambda m:f'<table width="{px(page_w)}"',html)
92
+ html=re.sub(r'<img[^>]+src="([^"]+\.png)"[^>]*>',
93
+ lambda m:f'<img src="{m.group(1)}" width="{int(page_w)}" />',html)
94
+ class PDF(FPDF,HTMLMixin): pass
95
+ pdf=PDF(); pdf.set_auto_page_break(True,15)
96
+ for s,p in [("",FONT_REG),("B",FONT_BLD),("I",FONT_REG),("BI",FONT_BLD)]:
97
+ pdf.add_font(FONT_NAME,s,str(p),uni=True)
98
+ pdf.set_fallback_fonts([FONT_NAME]); pdf.add_page()
99
+ pdf.set_font(FONT_NAME,"B",18); pdf.cell(0,12,"AI-Generated Business Report",ln=True); pdf.ln(3)
100
+ pdf.set_font(FONT_NAME,"",11); pdf.write_html(html)
 
 
 
 
 
 
 
 
 
101
  return bytes(pdf.output(dest="S"))
102
 
103
  @st.cache_resource(show_spinner=False)
104
+ def build_pptx(slide_texts: tuple, desc2png: dict) -> bytes:
105
+ prs=Presentation(); layout=prs.slide_layouts[1] # title+content
106
+ for idx,raw in enumerate(slide_texts):
107
+ raw=fix_bullets(raw); plain=re.sub(r'<.*?>','',raw).strip()
108
+ title_line, *body_lines = plain.splitlines()
109
+ body_lines=[l.strip().lstrip('*').strip() for l in body_lines if l.strip()]
110
+ slide=prs.slides.add_slide(layout)
111
+ slide.shapes.title.text=title_line
112
+ tf=slide.shapes.placeholders[1].text_frame
113
+ tf.clear()
114
+ for l in body_lines:
115
+ p=tf.add_paragraph(); p.text=l; p.font.size=Pt(18); p.level=0
116
+ m=re.search(r'<generate_chart:\s*"([^"]+)"\s*>', raw)
117
+ if m and m.group(1) in desc2png:
118
+ slide.shapes.add_picture(desc2png[m.group(1)], Inches(1), Inches(4), width=Inches(8))
119
+ fout=io.BytesIO(); prs.save(fout); return fout.getvalue()
120
 
121
+ @st.cache_resource(show_spinner=False)
122
+ def upload_pptx_to_slides(pptx_bytes: bytes) -> str|None:
123
+ if not CREDS_JSON: return None
124
+ creds=Credentials.from_service_account_file(
 
125
  CREDS_JSON,
126
  scopes=["https://www.googleapis.com/auth/drive.file",
127
  "https://www.googleapis.com/auth/presentations"])
128
+ drv=build("drive","v3",credentials=creds)
129
+ meta={"name":f"AI-Slides‐{uuid.uuid4().hex[:6]}",
130
+ "mimeType":"application/vnd.google-apps.presentation"}
131
+ media={"body":io.BytesIO(pptx_bytes),
132
+ "mimeType":"application/vnd.openxmlformats-officedocument.presentationml.presentation"}
133
+ f=drv.files().create(body=meta,media_body=media).execute()
134
  return f"https://docs.google.com/presentation/d/{f['id']}"
135
 
136
+ # ─────────────────────────────────────────────────────────────────────────────
137
+ # UI : INPUTS
138
+ # ─────────────────────────────────────────────────────────────────────────────
139
+ mode = st.radio("Output:", ["Report","Presentation","Both"],horizontal=True)
140
+ file = st.file_uploader("CSV / XLSX",["csv","xlsx"])
141
+ context = st.text_area("Optional business context / goals")
142
+ if not st.button("πŸš€ Generate"): st.stop()
143
+ if not file: st.warning("Upload data first"); st.stop()
144
+
145
+ # ─────────────────────────────────────────────────────────────────────────────
146
+ # LOAD DATAFRAME
147
+ # ─────────────────────────────────────────────────────────────────────────────
148
+ try:
149
+ st.session_state.df = pd.read_excel(file) if file.name.lower().endswith(".xlsx") else pd.read_csv(file)
150
+ except Exception as e:
151
+ st.error(f"Failed to read file: {e}"); st.stop()
152
+ df_hash = hash_df(st.session_state.df)
153
+ st.success(f"Loaded {st.session_state.df.shape[0]} rows Γ— {st.session_state.df.shape[1]} cols")
154
+
155
+ # ─────────────────────────────────────────────────────────────────────────────
156
+ # BUILD & RUN AGENTS
157
+ # ─────────────────────────────────────────────────────────────────────────────
158
+ subs=[]
159
+ if mode in ("Report","Both"):
160
+ subs.append(LlmAgent(name="ReportAgent",model="gemini-2.5-flash",
161
+ instruction="Write an exec-level Markdown report, using <generate_chart:\"…\"> placeholders."))
162
+ if mode in ("Presentation","Both"):
163
+ subs.append(LlmAgent(name="PresentationAgent",model="gemini-2.5-flash",
164
+ instruction=f"Create {SLIDE_COUNT} slides. Each begins 'Slide X:' and may include <generate_chart:\"…\"> placeholders."))
165
+
166
+ root=SequentialAgent(name="Pipeline",sub_agents=subs)
167
+ svc=InMemorySessionService(); session_id=str(uuid.uuid4())
168
+ asyncio.run(svc.create_session(app_name="app",user_id="usr",session_id=session_id))
169
+ runner=Runner(agent=root,app_name="app",session_service=svc)
170
+
171
+ ctx = {"dataset_info":{"shape": st.session_state.df.shape,
172
+ "columns": list(st.session_state.df.columns),
173
+ "dtypes": st.session_state.df.dtypes.astype(str).to_dict(),
174
+ "missing": st.session_state.df.isna().sum().to_dict()},
175
+ "user_context": context,
176
+ "preview": st.session_state.df.head().to_dict()}
177
+
178
+ final={}
179
+ async def _run():
180
+ async for ev in runner.run_async(user_id="usr",session_id=session_id,
181
+ new_message=types.Content(role="user",
182
+ parts=[types.Part(text=json.dumps(ctx))])):
183
+ if ev.is_final_response(): final[ev.author]=ev.content.parts[0].text
184
+ asyncio.run(_run())
185
+
186
+ # ─────────────────────────────────────────────────────────────────────────────
187
+ # CHARTS (one build for everything)
188
+ # ─────────────────────────────────────────────────────────────────────────────
189
+ all_md = "\n".join(final.values())
190
+ chart_descs = re.findall(r'<generate_chart:\s*"([^"]+)"\s*>', all_md)
191
+ png_map = build_charts(tuple(chart_descs), df_hash)
192
+
193
+ # ─────────────────────────────────────────────────────────────────────────────
194
+ # TABS
195
+ # ─────────────────────────────────────────────────────────────────────────────
196
+ if mode=="Both":
197
+ t_report,t_pres=st.tabs(["πŸ“„ Report","πŸ“‘ Slides"])
198
+ else:
199
+ t_report=t_pres=st
200
+
201
+ # ─────────────────────────────────────────────────────────────────────────────
202
+ # REPORT
203
+ # ─────────────────────────────────────────────────────────────────────────────
204
+ if mode in ("Report","Both"):
205
+ md_raw = fix_bullets(final["ReportAgent"])
206
+ md_prev=md_raw; md_pdf=md_raw
207
+ for d,p in png_map.items():
208
+ b64=base64.b64encode(open(p,"rb").read()).decode()
209
+ md_prev=re.sub(rf'<generate_chart:\s*"{re.escape(d)}"\s*>',
210
+ f'<img src="data:image/png;base64,{b64}" style="max-width:100%;margin:6px 0;" />',
211
+ md_prev)
212
+ md_pdf =re.sub(rf'<generate_chart:\s*"{re.escape(d)}"\s*>',
213
+ f'<img src="{p}" />',md_pdf)
214
+ with t_report:
215
+ st.markdown(md_prev,unsafe_allow_html=True)
216
+ if "pdf_bytes" not in st.session_state:
217
+ st.session_state.pdf_bytes=build_pdf(md_pdf)
218
+ st.download_button("⬇️ Download PDF", st.session_state.pdf_bytes,
219
+ "business_report.pdf","application/pdf")
220
+
221
+ # ─────────────────────────────────────────────────────────────────────────────
222
+ # PRESENTATION
223
+ # ─────────────────────────────────────────────────────────────────────────────
224
+ if mode in ("Presentation","Both"):
225
+ raw = fix_bullets(final["PresentationAgent"])
226
+ slides=[s.strip() for s in re.split(r'\bSlide\s+\d+:',raw) if s.strip()][:SLIDE_COUNT]
227
+ st.session_state.slides=slides
228
+ if "pptx_bytes" not in st.session_state:
229
+ st.session_state.pptx_bytes=build_pptx(tuple(slides), png_map)
230
+
231
+ if "idx" not in st.session_state: st.session_state.idx=0
232
+
233
+ def show_slide(i):
234
+ txt=slides[i]; st.markdown(f"### Slide {i+1}/{len(slides)}"); st.write(txt)
235
+ if st.button("πŸ”Š Speak",key=f"tts{i}"):
236
+ with st.spinner("Generating TTS…"):
237
+ aud,mime=generate_tts_audio(GEMINI,txt)
238
+ if aud:
239
+ st.audio(pcm_to_wav(aud) if 'pcm' in mime or 'L16' in mime else aud,
240
+ format="audio/wav" if 'pcm' in mime or 'L16' in mime else mime)
241
+
242
+ with t_pres:
243
+ c1,c2=st.columns(2)
244
+ c1.button("⬅️",disabled=st.session_state.idx==0,
245
+ on_click=lambda: st.session_state.__setitem__("idx",st.session_state.idx-1))
246
+ c2.button("➑️",disabled=st.session_state.idx==len(slides)-1,
247
+ on_click=lambda: st.session_state.__setitem__("idx",st.session_state.idx+1))
248
+ show_slide(st.session_state.idx)
249
+
250
+ st.download_button("⬇️ PPTX", st.session_state.pptx_bytes,
251
+ "presentation.pptx",
252
+ ("application/vnd.openxmlformats-officedocument."
253
+ "presentationml.presentation"))
254
+ if st.button("⬆️ Upload to Google Slides"):
255
+ with st.spinner("Uploading…"):
256
+ url=upload_pptx_to_slides(st.session_state.pptx_bytes)
257
+ url and st.success(f"[Open Slides]({url})")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
258
  ###############################################################################
259
  # End
260
  ###############################################################################