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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +143 -217
app.py CHANGED
@@ -1,260 +1,186 @@
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
24
- from google.adk.runners import Runner
25
  from google.adk.sessions import InMemorySessionService
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
  ###############################################################################
 
1
  ###############################################################################
2
+ # app.py β€” stable, cached, with on-demand TTS
3
  ###############################################################################
4
+ import os, re, json, uuid, asyncio, base64, io, hashlib, tempfile
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"); import matplotlib.pyplot as plt
 
8
  from fpdf import FPDF, HTMLMixin
9
  from markdown import markdown
10
  try: from markdown_it import MarkdownIt
11
  except ImportError: MarkdownIt = None
12
  from pptx import Presentation
13
  from pptx.util import Inches, Pt
 
 
 
 
 
 
14
  from google import genai
15
  from google.genai import types
16
+ from google.adk.agents import LlmAgent, SequentialAgent
17
+ from google.adk.runners import Runner
18
  from google.adk.sessions import InMemorySessionService
19
  from langchain_experimental.agents import create_pandas_dataframe_agent
20
+ from langchain_google_genai import ChatGoogleGenerativeAI
21
 
22
+ # β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” CONFIG
 
 
23
  FONT_DIR = Path(__file__).parent
24
+ FONT_REG = FONT_DIR/"NotoSans-Regular.ttf"; FONT_BLD = FONT_DIR/"NotoSans-Bold.ttf"
25
+ FONT = "NotoSans"; SLIDES = 7
26
+ TTS_MODEL = "gemini-2.5-flash-preview-tts"
 
 
 
 
 
 
27
 
28
+ API_KEY = os.getenv("GEMINI_API_KEY"); st.experimental_set_query_params()
29
  if not API_KEY: st.error("Set GEMINI_API_KEY"); st.stop()
30
+ GEM = genai.Client(api_key=API_KEY)
31
+
32
+ # β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” HELPERS
33
+ fix_bullet = lambda t: t.replace("\x95","β€’")
34
+ def pcm_to_wav(raw, rate=24_000):
35
+ import wave, contextlib; buf=io.BytesIO()
36
+ with contextlib.closing(wave.open(buf,'wb')) as w:
37
+ w.setnchannels(1); w.setsampwidth(2); w.setframerate(rate); w.writeframes(raw)
 
 
 
 
 
 
 
38
  return buf.getvalue()
39
+ def df_hash(df): return hashlib.sha1(pd.util.hash_pandas_object(df, index=True).values).hexdigest()
40
+
41
+ @st.cache_data(show_spinner=False)
42
+ def tts_cached(text):
43
+ txt = text[:500]+"…" if len(text)>500 else text
44
+ try:
45
+ r=GEM.models.generate_content(
46
+ model=TTS_MODEL, contents=f"Say clearly: {txt}",
47
+ config=types.GenerateContentConfig(
48
+ response_modalities=["AUDIO"],
49
+ speech_config=types.SpeechConfig(
50
+ voice_config=types.VoiceConfig(
51
+ prebuilt_voice_config=types.PrebuiltVoiceConfig(voice_name="Kore")))))
52
+ audio=r.candidates[0].content.parts[0].inline_data
53
+ return audio.data, audio.mime_type
54
+ except Exception as e: st.error(f"TTS error: {e}"); return None,None
55
+
56
+ # β€” cached heavy builders β€”
57
  @st.cache_resource(show_spinner=False)
58
+ def pandas_agent(dfh): return create_pandas_dataframe_agent(
59
+ llm=ChatGoogleGenerativeAI(model="gemini-2.0-flash",google_api_key=API_KEY),
60
+ df=st.session_state.df, verbose=False, allow_dangerous_code=True)
 
 
61
 
62
  @st.cache_resource(show_spinner=False)
63
+ def charts(descs, dfh):
64
+ pa=pandas_agent(dfh); mp={}
 
65
  for d in descs:
66
+ pa.run(f"Create a {d} using matplotlib")
 
67
  p=Path(tempfile.gettempdir())/f"{uuid.uuid4()}.png"
68
+ plt.gcf().savefig(p,dpi=300,bbox_inches="tight"); plt.close()
69
+ mp[d]=str(p)
70
+ return mp
71
 
72
  @st.cache_resource(show_spinner=False)
73
+ def build_pdf(html_md):
74
+ html_md=fix_bullet(html_md)
75
+ html=(MarkdownIt("commonmark",{"breaks":True}).enable("table").render(html_md)
76
+ if MarkdownIt else markdown(html_md,extensions=["tables"]))
77
+ dummy=FPDF(); width=dummy.w-2*dummy.l_margin; px=lambda mm:int(mm*dummy.k)
78
  html=re.sub(r'(<table[^>]*>)',r'\1<font size="8">',html).replace('</table>','</font></table>')
79
+ html=re.sub(r'<table(?![^>]*width=)',lambda m:f'<table width="{px(width)}"',html)
80
  html=re.sub(r'<img[^>]+src="([^"]+\.png)"[^>]*>',
81
+ lambda m:f'<img src="{m.group(1)}" width="{int(width)}" />',html)
82
  class PDF(FPDF,HTMLMixin): pass
83
  pdf=PDF(); pdf.set_auto_page_break(True,15)
84
+ for s,f in [("",FONT_REG),("B",FONT_BLD),("I",FONT_REG),("BI",FONT_BLD)]: pdf.add_font(FONT,s,str(f),uni=True)
85
+ pdf.set_fallback_fonts([FONT]); pdf.add_page()
86
+ pdf.set_font(FONT,"B",18); pdf.cell(0,12,"AI-Generated Business Report",ln=True); pdf.ln(3)
87
+ pdf.set_font(FONT,"",11); pdf.write_html(html)
 
88
  return bytes(pdf.output(dest="S"))
89
 
90
  @st.cache_resource(show_spinner=False)
91
+ def build_pptx(slides, mp):
92
+ prs=Presentation(); layout=prs.slide_layouts[1]
93
+ for i,raw in enumerate(slides):
94
+ raw=fix_bullet(raw)
95
+ title,*body=filter(None,[l.strip().lstrip('*').strip() for l in re.sub(r'<.*?>','',raw).splitlines()])
96
+ sl=prs.slides.add_slide(layout); sl.shapes.title.text=title
97
+ tf=sl.placeholders[1].text_frame; tf.clear()
98
+ for line in body: p=tf.add_paragraph(); p.text=line; p.font.size=Pt(24)
99
+ m=re.search(r'<generate_chart:\s*"([^"]+)"',raw)
100
+ if m and m.group(1) in mp:
101
+ sl.shapes.add_picture(mp[m.group(1)],Inches(1),Inches(4),width=Inches(8))
102
+ buf=io.BytesIO(); prs.save(buf); return buf.getvalue()
103
+
104
+ # β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” UI INPUT
105
+ mode = st.radio("Output:",["Report","Presentation","Both"],horizontal=True)
106
+ upl = st.file_uploader("CSV / XLSX",["csv","xlsx"])
107
+ ctx = st.text_area("Optional business context")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  if not st.button("πŸš€ Generate"): st.stop()
109
+ if not upl: st.warning("Upload data"); st.stop()
110
+
111
+ # β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” HEAVY GEN
112
+ if "generated" not in st.session_state:
113
+ try:
114
+ st.session_state.df = pd.read_excel(upl) if upl.name.lower().endswith(".xlsx") else pd.read_csv(upl)
115
+ except Exception as e: st.error(e); st.stop()
116
+ h = df_hash(st.session_state.df)
117
+ # Agents
118
+ agents=[]
119
+ if mode in ("Report","Both"):
120
+ agents.append(LlmAgent(name="ReportAgent",model="gemini-2.5-flash",
121
+ instruction="Write exec-level Markdown report with <generate_chart:\"…\">"))
122
+ if mode in ("Presentation","Both"):
123
+ agents.append(LlmAgent(name="PresentationAgent",model="gemini-2.5-flash",
124
+ instruction=f"Create {SLIDES} slides starting 'Slide X:' with <generate_chart:\"…\"> tags"))
125
+ root=SequentialAgent(name="root",sub_agents=agents)
126
+ svc=InMemorySessionService(); sid=str(uuid.uuid4())
127
+ asyncio.run(svc.create_session(app_name="app",user_id="u",session_id=sid))
128
+ runner=Runner(agent=root,app_name="app",session_service=svc)
129
+ context={"dataset_info":{"shape":st.session_state.df.shape},
130
+ "user_context":ctx,"preview":st.session_state.df.head().to_dict()}
131
+ result={}
132
+ async def _go():
133
+ async for ev in runner.run_async(user_id="u",session_id=sid,
134
+ new_message=types.Content(role="user",
135
+ parts=[types.Part(text=json.dumps(context))])):
136
+ if ev.is_final_response(): result[ev.author]=ev.content.parts[0].text
137
+ asyncio.run(_go())
138
+ st.session_state.result=result
139
+ # charts
140
+ tags=re.findall(r'<generate_chart:\s*"([^"]+)"',"\n".join(result.values()))
141
+ mp=charts(tuple(tags),h); st.session_state.chart_map=mp
142
+ # slides & docs
143
+ if "ReportAgent" in result:
144
+ md=result["ReportAgent"]
145
+ md_full=md
146
+ for d,p in mp.items(): md_full=re.sub(rf'<generate_chart:\s*"{re.escape(d)}"',f'<img src="{p}"',md_full)
147
+ st.session_state.pdf=build_pdf(md_full)
148
+ if "PresentationAgent" in result:
149
+ slides=[s.strip() for s in re.split(r'\bSlide\s+\d+:',result["PresentationAgent"]) if s.strip()][:SLIDES]
150
+ st.session_state.slides=slides
151
+ st.session_state.pptx=build_pptx(tuple(slides),mp)
152
+ st.session_state.idx=0
153
+ st.session_state.generated=True
154
+
155
+ # β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€” TABS UI
156
+ if mode=="Both": tab_rep,tab_pre=st.tabs(["Report","Slides"])
157
+ else: tab_rep=tab_pre=st
158
 
 
 
 
 
 
 
 
 
 
 
 
159
  if mode in ("Report","Both"):
160
+ md_prev=st.session_state.result["ReportAgent"]
161
+ for d,p in st.session_state.chart_map.items():
 
162
  b64=base64.b64encode(open(p,"rb").read()).decode()
163
+ md_prev=re.sub(rf'<generate_chart:\s*"{re.escape(d)}"',f'<img src="data:image/png;base64,{b64}"',md_prev)
164
+ with tab_rep:
 
 
 
 
165
  st.markdown(md_prev,unsafe_allow_html=True)
166
+ st.download_button("Download PDF",st.session_state.pdf,"report.pdf","application/pdf")
 
 
 
167
 
 
 
 
168
  if mode in ("Presentation","Both"):
169
+ with tab_pre:
170
+ slides=st.session_state.slides
171
+ idx=st.session_state.idx
172
+ st.markdown(f"### Slide {idx+1}/{len(slides)}")
173
+ st.write(fix_bullet(slides[idx]))
174
+ if st.button("πŸ”Š Speak",key="speak"):
175
+ with st.spinner("TTS…"):
176
+ aud,mime=tts_cached(slides[idx])
 
 
 
 
 
177
  if aud:
178
+ st.audio(pcm_to_wav(aud) if mime.startswith("audio/raw") else aud,
179
+ format="audio/wav" if mime.startswith("audio/raw") else mime)
180
+ col1,col2=st.columns(2)
181
+ if col1.button("⬅️",disabled=idx==0): st.session_state.idx-=1; st.rerun()
182
+ if col2.button("➑️",disabled=idx==len(slides)-1): st.session_state.idx+=1; st.rerun()
183
+ st.download_button("Download PPTX",st.session_state.pptx,
 
 
 
 
 
 
184
  "presentation.pptx",
185
+ "application/vnd.openxmlformats-officedocument.presentationml.presentation")
 
 
 
 
 
 
 
186
  ###############################################################################