Rajan Sharma commited on
Commit
da71da2
·
verified ·
1 Parent(s): 889b6f3

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +292 -194
app.py CHANGED
@@ -1,241 +1,339 @@
1
- # app.py
2
  from __future__ import annotations
 
3
  import os
 
4
  import traceback
5
- import regex as re2
 
6
  from typing import List, Dict, Any
7
 
 
8
  import gradio as gr
9
  import pandas as pd
10
- from datetime import datetime
11
 
12
  # --- BACKEND IMPORTS ---
13
- from langchain_cohere import ChatCohere
14
- from langchain_community.utilities.python import PythonREPL
 
15
 
16
  # --- LOCAL MODULE IMPORTS ---
17
  from settings import (
18
- HEALTHCARE_SETTINGS, GENERAL_CONVERSATION_PROMPT,
19
- COHERE_MODEL_PRIMARY, COHERE_TIMEOUT_S, USE_OPEN_FALLBACKS
 
 
 
20
  )
21
  from audit_log import log_event
22
  from privacy import safety_filter, refusal_reply
23
  from llm_router import cohere_chat, _co_client, cohere_embed
24
 
25
- # --- UTILITY FUNCTIONS ---
 
 
 
26
 
27
  def load_markdown_text(filepath: str) -> str:
28
- """Safely loads text content from a markdown file."""
29
  try:
30
- with open(filepath, 'r', encoding='utf-8') as f:
31
  return f.read()
32
  except FileNotFoundError:
33
  return f"**Error:** The document `{os.path.basename(filepath)}` was not found."
34
 
35
- def _sanitize_text(s: str) -> str:
36
- if not isinstance(s, str): return s
37
- return re2.sub(r'[\p{C}--[\n\t]]+', '', s)
38
 
39
- def _create_python_script(user_scenario: str, schema_context: str) -> str:
40
- """Uses an LLM to act as an "AI Coder", writing a complete Python script."""
41
- prompt_for_coder = f"""
42
- You are an expert Python data scientist. Your sole job is to write a single, complete, and executable Python script to answer the user's request.
43
- You have access to a list of pandas dataframes loaded into a variable named `dfs`. The first dataframe is `dfs[0]`, the second is `dfs[1]`, and so on.
44
 
45
- CRITICAL CONTEXT: Before writing any code, you MUST first understand the data you have been given. Here is the schema for each dataframe:
46
- --- DATA SCHEMA ---
47
- {schema_context}
48
- --- END SCHEMA ---
49
 
50
- Based on the user's scenario below, write a single Python script that performs the entire analysis.
 
 
51
 
52
- RULES FOR YOUR SCRIPT:
53
- 1. **Use the DataFrames:** Your script MUST use the `dfs` list to access the data.
54
- 2. **Print Your Findings:** Use the `print()` function at each step of your analysis to output the results. The final output of your script should be the complete, formatted report.
55
- 3. **No Placeholders:** Do not use placeholder data. Your code must perform the real calculations.
56
- 4. **Self-Contained:** The script must be entirely self-contained.
57
 
58
- --- USER'S SCENARIO ---
59
- {user_scenario}
 
60
 
61
- --- PYTHON SCRIPT ---
62
- ```python
63
- import pandas as pd
 
 
 
 
64
 
65
  def analyze_data(dfs):
66
  try:
67
- # Your generated Python code will go here.
68
- pass
69
  except Exception as e:
70
  print(f"An error occurred during analysis: {{e}}")
71
- Now, write the complete Python script inside the try block.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  """
73
- generated_text = cohere_chat(prompt_for_coder)
74
- match = re2.search(r"python\n(.*?)", generated_text, re2.DOTALL)
75
- code
76
- Code
77
- # --- THIS BLOCK IS NOW CORRECTLY INDENTED ---
78
- if match:
79
- script_content = match.group(1).strip()
80
- script_content = script_content.replace("def analyze_data(dfs):", "", 1)
81
- script_content = "\n".join([line for line in script_content.split('\n') if line.strip() != "pass"])
82
- return script_content.strip()
83
- else:
84
- return "print('Error: The AI failed to generate a valid Python script.')"
85
- def _append_msg(history_messages: List[Dict[str, str]], role: str, content: str) -> List[Dict[str, str]]:
86
- return (history_messages or []) + [{"role": role, "content": content}]
 
 
87
  def ping_cohere() -> str:
88
- """Lightweight health check against Cohere."""
89
- try:
90
- cli = _co_client()
91
- if not cli: return "Cohere client not initialized. Is COHERE_API_KEY set?"
92
- vecs = cohere_embed(["hello", "world"])
93
- return f"Cohere OK ✅ (model={COHERE_MODEL_PRIMARY}, timeout={COHERE_TIMEOUT_S}s)" if vecs else "Cohere reachable."
94
- except Exception as e:
95
- return f"Cohere ping failed: {e}"
96
- --- THE CORE ANALYSIS ENGINE ---
 
 
 
 
 
 
 
 
97
  def handle(user_msg: str, files: list) -> str:
98
- """This is the powerful backend engine using the "Coder" pattern."""
99
- try:
100
- safe_in, blocked_in, reason_in = safety_filter(user_msg, mode="input")
101
- if blocked_in: return refusal_reply(reason_in)
102
- code
103
- Code
104
- file_paths: List[str] = [getattr(f, "name", None) or f for f in (files or [])]
105
-
106
- if file_paths:
107
- dataframes = []
108
- schema_parts = []
109
- for i, p in enumerate(file_paths):
110
- if p.endswith('.csv'):
111
- try:
112
- df = pd.read_csv(p)
113
- dataframes.append(df)
114
- schema_parts.append(f"DataFrame `dfs[{i}]` (from file `{os.path.basename(p)}`):\n{df.head().to_markdown()}\n")
115
- except UnicodeDecodeError:
116
- print(f"Warning: Reading {os.path.basename(p)} with fallback latin1 encoding.")
117
- df = pd.read_csv(p, encoding='latin1')
 
 
 
 
 
 
118
  dataframes.append(df)
119
- schema_parts.append(f"DataFrame `dfs[{i}]` (from file `{os.path.basename(p)}`):\n{df.head().to_markdown()}\n")
120
-
121
- if not dataframes: return "Please upload at least one CSV file."
 
122
 
123
- schema_context = "\n".join(schema_parts)
124
- analysis_script_logic = _create_python_script(safe_in, schema_context)
125
 
126
- python_repl = PythonREPL()
127
- full_script_to_run = f"""
 
 
 
128
  import pandas as pd
 
129
  def analyze_data(dfs):
130
- try:
131
- {analysis_script_logic}
132
- except Exception as e:
133
- print(f"An error occurred during analysis: {{e}}")
134
- analyze_data(dfs)
135
  """
136
- local_vars = {"dfs": dataframes}
137
- try:
138
- res = python_repl.run(command=full_script_to_run, locals=local_vars)
139
- return _sanitize_text(res)
140
- except Exception as e:
141
- return f"An error occurred while executing the AI-generated script: {e}"
142
- else:
143
- prompt = f"{GENERAL_CONVERSATION_PROMPT}\n\nUser: {safe_in}\nAssistant:"
144
- return _sanitize_text(cohere_chat(prompt) or "How can I help further?")
145
- code
146
- Code
147
- except Exception as e:
148
- tb = traceback.format_exc()
149
- log_event("app_error", None, {"err": str(e), "tb": tb})
150
- return f"A critical error occurred: {e}"
151
- --- PRE-LOAD LEGAL DOCUMENTS ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152
  PRIVACY_POLICY_TEXT = load_markdown_text("privacy_policy.md")
153
  TERMS_OF_SERVICE_TEXT = load_markdown_text("terms_of_service.md")
154
- ---------------- THE PROFESSIONAL UI WITH INTEGRATED LEGAL DOCS ----------------
155
  with gr.Blocks(theme="soft", css="style.css") as demo:
156
- assessment_history = gr.State([])
157
- code
158
- Code
159
- with gr.Group(visible=False) as privacy_modal:
160
- with gr.Blocks():
161
- gr.Markdown(PRIVACY_POLICY_TEXT)
162
- close_privacy_btn = gr.Button("Close")
163
-
164
- with gr.Group(visible=False) as terms_modal:
165
- with gr.Blocks():
166
- gr.Markdown(TERMS_OF_SERVICE_TEXT)
167
- close_terms_btn = gr.Button("Close")
168
-
169
- gr.Markdown("# Universal AI Data Analyst")
170
- with gr.Row(variant="panel"):
171
- with gr.Column(scale=1):
172
- gr.Markdown("## New Assessment")
173
- files_input = gr.Files(label="Upload Data Files (.csv)", file_count="multiple", type="filepath", file_types=[".csv"])
174
- prompt_input = gr.Textbox(label="Prompt", placeholder="Paste your scenario here.", lines=15)
175
- with gr.Row():
176
- send_btn = gr.Button("▶️ Run Analysis", variant="primary", scale=2)
177
- clear_btn = gr.Button("🗑️ Clear")
178
- ping_btn = gr.Button("Ping Cohere")
179
- ping_out = gr.Markdown()
180
- with gr.Column(scale=2):
181
- with gr.Tabs():
182
- with gr.TabItem("Current Assessment", id=0):
183
- chat_history_output = gr.Chatbot(label="Analysis Output", type="messages", height=600)
184
- with gr.TabItem("Assessment History", id=1):
185
- gr.Markdown("## Review Past Assessments")
186
- history_dropdown = gr.Dropdown(label="Select an assessment to review", choices=[])
187
- history_display = gr.Markdown(label="Selected Assessment Details")
188
- with gr.Row(): gr.Markdown("---")
189
- with gr.Row():
190
- privacy_link = gr.Button("Privacy Policy", variant="link")
191
- terms_link = gr.Button("Terms of Service", variant="link")
192
-
193
- def run_analysis_wrapper(prompt, files, chat_history_list, history_state_list):
194
- if not prompt or not files:
195
- gr.Warning("Please provide both a prompt and at least one data file.")
196
- yield chat_history_list, history_state_list, gr.update()
197
- return
198
-
199
- chat_with_user_msg = _append_msg(chat_history_list, "user", prompt)
200
- thinking_message = _append_msg(chat_with_user_msg, "assistant", "```\n🧠 Generating analysis script... This may take a moment.\n```")
201
- yield thinking_message, history_state_list, gr.update()
202
-
203
- ai_response_text = handle(prompt, files)
204
-
205
- final_chat = _append_msg(chat_with_user_msg, "assistant", ai_response_text)
206
- timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
207
- file_names = [os.path.basename(f.name if hasattr(f, 'name') else f) for f in files]
208
- new_assessment = {"id": timestamp, "prompt": prompt, "files": file_names, "response": ai_response_text}
209
- updated_history = history_state_list + [new_assessment]
210
- history_labels = [f"{item['id']} - {item['prompt'][:40]}..." for item in updated_history]
211
- yield final_chat, updated_history, gr.update(choices=history_labels)
212
-
213
- def view_history(selection, history_state_list):
214
- if not selection or not history_state_list: return ""
215
- selected_id = selection.split(" - ")[0]
216
- selected_assessment = next((item for item in history_state_list if item["id"] == selected_id), None)
217
- if selected_assessment:
218
- file_list_md = "\n- ".join(selected_assessment['files'])
219
- return f"""### Assessment from: {selected_assessment['id']}\n**Files Used:**\n- {file_list_md}\n---\n**Original Prompt:**\n> {selected_assessment['prompt']}\n---\n**AI Generated Response:**\n{selected_assessment['response']}"""
220
- return "Could not find the selected assessment."
221
-
222
- send_btn.click(
223
- run_analysis_wrapper,
224
- inputs=[prompt_input, files_input, chat_history_output, assessment_history],
225
- outputs=[chat_history_output, assessment_history, history_dropdown]
226
- )
227
- history_dropdown.change(
228
- view_history,
229
- inputs=[history_dropdown, assessment_history],
230
- outputs=[history_display]
231
- )
232
- clear_btn.click(lambda: (None, None, [], []), outputs=[prompt_input, files_input, chat_history_output, assessment_history])
233
- ping_btn.click(ping_cohere, outputs=[ping_out])
234
- privacy_link.click(lambda: gr.update(visible=True), outputs=[privacy_modal])
235
- close_privacy_btn.click(lambda: gr.update(visible=False), outputs=[privacy_modal])
236
- terms_link.click(lambda: gr.update(visible=True), outputs=[terms_modal])
237
- close_terms_btn.click(lambda: gr.update(visible=False), outputs=[terms_modal])
238
- if name == "main":
239
- if not os.getenv("COHERE_API_KEY"):
240
- print("🔴 COHERE_API_KEY environment variable not set. Application may not function correctly.")
241
- demo.launch(server_name="0.0.0.0", server_port=int(os.getenv("PORT", "7860")))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  from __future__ import annotations
2
+
3
  import os
4
+ import io
5
  import traceback
6
+ from contextlib import redirect_stdout
7
+ from datetime import datetime
8
  from typing import List, Dict, Any
9
 
10
+ import regex as re # pip install regex
11
  import gradio as gr
12
  import pandas as pd
 
13
 
14
  # --- BACKEND IMPORTS ---
15
+ from langchain_cohere import ChatCohere # kept for compatibility if you use it elsewhere
16
+ # If you still want PythonREPL later, you can add it back:
17
+ # from langchain_community.utilities.python import PythonREPL
18
 
19
  # --- LOCAL MODULE IMPORTS ---
20
  from settings import (
21
+ HEALTHCARE_SETTINGS,
22
+ GENERAL_CONVERSATION_PROMPT,
23
+ COHERE_MODEL_PRIMARY,
24
+ COHERE_TIMEOUT_S,
25
+ USE_OPEN_FALLBACKS,
26
  )
27
  from audit_log import log_event
28
  from privacy import safety_filter, refusal_reply
29
  from llm_router import cohere_chat, _co_client, cohere_embed
30
 
31
+
32
+ # =========================
33
+ # Utility Helpers
34
+ # =========================
35
 
36
  def load_markdown_text(filepath: str) -> str:
37
+ """Safely load text content from a markdown file."""
38
  try:
39
+ with open(filepath, "r", encoding="utf-8") as f:
40
  return f.read()
41
  except FileNotFoundError:
42
  return f"**Error:** The document `{os.path.basename(filepath)}` was not found."
43
 
 
 
 
44
 
45
+ def _sanitize_text(s: Any) -> str:
46
+ """Remove control chars (except tabs/newlines) to keep UI stable."""
47
+ if not isinstance(s, str):
48
+ s = str(s)
49
+ return re.sub(r"[\p{C}--[\n\t]]+", "", s)
50
 
 
 
 
 
51
 
52
+ def _indent(s: str, spaces: int) -> str:
53
+ pad = " " * spaces
54
+ return "\n".join((pad + line) if line.strip() else line for line in s.splitlines())
55
 
 
 
 
 
 
56
 
57
+ # =========================
58
+ # AI Coding Path
59
+ # =========================
60
 
61
+ def _create_python_script(user_scenario: str, schema_context: str) -> str:
62
+ """
63
+ Ask the model to produce a single Python snippet that goes INSIDE a try: block of analyze_data(dfs).
64
+ We extract the content between ```python ... ``` fences if present.
65
+ """
66
+ prompt_for_coder = f"""
67
+ You are an expert Python data scientist. Your sole job is to write a single, complete analysis that will be inserted INSIDE:
68
 
69
  def analyze_data(dfs):
70
  try:
71
+ # YOUR CODE HERE
 
72
  except Exception as e:
73
  print(f"An error occurred during analysis: {{e}}")
74
+
75
+ RULES:
76
+ 1) Use the provided list `dfs` of pandas DataFrames (dfs[0], dfs[1], ...).
77
+ 2) Print results at each major step with print().
78
+ 3) No placeholders; operate on real data in dfs.
79
+ 4) The code you return must be valid Python and indentation-safe.
80
+ 5) Do NOT redefine analyze_data; only provide the body INSIDE the try: block.
81
+
82
+ --- DATA SCHEMA (heads) ---
83
+ {schema_context}
84
+ --- END SCHEMA ---
85
+
86
+ --- USER SCENARIO ---
87
+ {user_scenario}
88
+
89
+ Return only a Python code block, fenced as ```python ... ``` containing the body that goes inside the try: block.
90
  """
91
+ generated_text = cohere_chat(prompt_for_coder) or ""
92
+ # Prefer fenced code
93
+ fence = re.search(r"```python\s*(.*?)```", generated_text, re.DOTALL | re.IGNORECASE)
94
+ if fence:
95
+ body = fence.group(1).strip()
96
+ else:
97
+ # Fallback: take everything, best-effort trim if model didn’t fence
98
+ body = generated_text.strip()
99
+
100
+ # Strip any accidental wrapper definitions the model might add
101
+ # e.g., remove "def analyze_data(dfs):" and a nested try:/except: if present
102
+ body = re.sub(r"^def\s+analyze_data\s*\(.*?\):\s*", "", body)
103
+ # We keep user's try/except if they provided, but usually we want raw steps.
104
+ return body.strip() or "print('Error: No analysis steps were generated.')"
105
+
106
+
107
  def ping_cohere() -> str:
108
+ """Lightweight health check against Cohere."""
109
+ try:
110
+ cli = _co_client()
111
+ if not cli:
112
+ return "Cohere client not initialized. Is COHERE_API_KEY set?"
113
+ vecs = cohere_embed(["hello", "world"])
114
+ if vecs:
115
+ return f"Cohere OK (model={COHERE_MODEL_PRIMARY}, timeout={COHERE_TIMEOUT_S}s)"
116
+ return "Cohere reachable."
117
+ except Exception as e:
118
+ return f"Cohere ping failed: {e}"
119
+
120
+
121
+ # =========================
122
+ # Core Analysis Engine
123
+ # =========================
124
+
125
  def handle(user_msg: str, files: list) -> str:
126
+ """
127
+ Main backend engine using the 'Coder pattern':
128
+ - Safety check
129
+ - Load CSVs -> dfs
130
+ - Build schema heads
131
+ - Ask the model for analysis code (body only)
132
+ - Execute analyze_data(dfs) in a safe, isolated namespace
133
+ - Return captured stdout
134
+ """
135
+ try:
136
+ safe_in, blocked_in, reason_in = safety_filter(user_msg, mode="input")
137
+ if blocked_in:
138
+ return refusal_reply(reason_in)
139
+
140
+ # Resolve file paths (Gradio may give temp File objects or strings)
141
+ file_paths: List[str] = [getattr(f, "name", None) or f for f in (files or [])]
142
+
143
+ if file_paths:
144
+ dataframes: List[pd.DataFrame] = []
145
+ schema_parts: List[str] = []
146
+ for i, p in enumerate(file_paths):
147
+ if isinstance(p, str) and p.lower().endswith(".csv"):
148
+ try:
149
+ df = pd.read_csv(p)
150
+ except UnicodeDecodeError:
151
+ df = pd.read_csv(p, encoding="latin1")
152
  dataframes.append(df)
153
+ head_md = df.head().to_markdown() if not df.empty else "(empty dataframe)"
154
+ schema_parts.append(
155
+ f"DataFrame dfs[{i}] from `{os.path.basename(p)}`:\n{head_md}\n"
156
+ )
157
 
158
+ if not dataframes:
159
+ return "Please upload at least one CSV file."
160
 
161
+ schema_context = "\n".join(schema_parts)
162
+ analysis_body = _create_python_script(safe_in, schema_context)
163
+
164
+ # Assemble the full script to exec
165
+ script = f"""
166
  import pandas as pd
167
+
168
  def analyze_data(dfs):
169
+ try:
170
+ {_indent(analysis_body, 8)}
171
+ except Exception as e:
172
+ print(f"An error occurred during analysis: {{e}}")
 
173
  """
174
+
175
+ # Execute in isolated namespace and capture stdout
176
+ ns: Dict[str, Any] = {}
177
+ ns["dfs"] = dataframes # make dfs available inside exec scope
178
+
179
+ buf = io.StringIO()
180
+ try:
181
+ with redirect_stdout(buf):
182
+ exec(script, ns, ns)
183
+ # call analyze_data(dfs)
184
+ ns["analyze_data"](ns["dfs"])
185
+ except Exception as e:
186
+ return _sanitize_text(f"An error occurred while executing the AI-generated script:\n{e}\n\nTraceback:\n{traceback.format_exc()}")
187
+
188
+ output = buf.getvalue()
189
+ return _sanitize_text(output or "(No output produced by the analysis.)")
190
+
191
+ # No files: fall back to general conversation
192
+ prompt = f"{GENERAL_CONVERSATION_PROMPT}\n\nUser: {safe_in}\nAssistant:"
193
+ resp = cohere_chat(prompt) or "How can I help further?"
194
+ return _sanitize_text(resp)
195
+
196
+ except Exception as e:
197
+ tb = traceback.format_exc()
198
+ log_event("app_error", None, {"err": str(e), "tb": tb})
199
+ return f"A critical error occurred: {e}"
200
+
201
+
202
+ # =========================
203
+ # UI (Gradio)
204
+ # =========================
205
+
206
  PRIVACY_POLICY_TEXT = load_markdown_text("privacy_policy.md")
207
  TERMS_OF_SERVICE_TEXT = load_markdown_text("terms_of_service.md")
208
+
209
  with gr.Blocks(theme="soft", css="style.css") as demo:
210
+ assessment_history = gr.State([]) # list[dict]: id, prompt, files, response
211
+
212
+ # Modals
213
+ with gr.Group(visible=False) as privacy_modal:
214
+ with gr.Blocks():
215
+ gr.Markdown(PRIVACY_POLICY_TEXT)
216
+ close_privacy_btn = gr.Button("Close")
217
+
218
+ with gr.Group(visible=False) as terms_modal:
219
+ with gr.Blocks():
220
+ gr.Markdown(TERMS_OF_SERVICE_TEXT)
221
+ close_terms_btn = gr.Button("Close")
222
+
223
+ gr.Markdown("# Universal AI Data Analyst")
224
+
225
+ with gr.Row(variant="panel"):
226
+ with gr.Column(scale=1):
227
+ gr.Markdown("## New Assessment")
228
+ files_input = gr.Files(
229
+ label="Upload Data Files (.csv)",
230
+ file_count="multiple",
231
+ type="filepath",
232
+ file_types=[".csv"],
233
+ )
234
+ prompt_input = gr.Textbox(
235
+ label="Prompt",
236
+ placeholder="Paste your scenario here.",
237
+ lines=15,
238
+ )
239
+ with gr.Row():
240
+ send_btn = gr.Button("▶️ Run Analysis", variant="primary", scale=2)
241
+ clear_btn = gr.Button("🗑️ Clear")
242
+ ping_btn = gr.Button("Ping Cohere")
243
+ ping_out = gr.Markdown()
244
+
245
+ with gr.Column(scale=2):
246
+ with gr.Tabs():
247
+ with gr.TabItem("Current Assessment", id=0):
248
+ chat_history_output = gr.Chatbot(
249
+ label="Analysis Output",
250
+ type="messages",
251
+ height=600,
252
+ )
253
+ with gr.TabItem("Assessment History", id=1):
254
+ gr.Markdown("## Review Past Assessments")
255
+ history_dropdown = gr.Dropdown(
256
+ label="Select an assessment to review",
257
+ choices=[],
258
+ )
259
+ history_display = gr.Markdown(label="Selected Assessment Details")
260
+
261
+ with gr.Row():
262
+ gr.Markdown("---")
263
+
264
+ with gr.Row():
265
+ privacy_link = gr.Button("Privacy Policy", variant="link")
266
+ terms_link = gr.Button("Terms of Service", variant="link")
267
+
268
+ # ---------- Callbacks ----------
269
+
270
+ def run_analysis_wrapper(prompt, files, chat_history_list, history_state_list):
271
+ if not prompt or not files:
272
+ gr.Warning("Please provide both a prompt and at least one data file.")
273
+ yield chat_history_list, history_state_list, gr.update()
274
+ return
275
+
276
+ chat_with_user_msg = (chat_history_list or []) + [{"role": "user", "content": prompt}]
277
+ thinking_message = chat_with_user_msg + [
278
+ {"role": "assistant", "content": "```\n🧠 Generating analysis script... This may take a moment.\n```"}
279
+ ]
280
+ yield thinking_message, history_state_list, gr.update()
281
+
282
+ ai_response_text = handle(prompt, files)
283
+
284
+ final_chat = chat_with_user_msg + [{"role": "assistant", "content": ai_response_text}]
285
+ timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
286
+ file_names = [os.path.basename(getattr(f, "name", f)) for f in files]
287
+ new_assessment = {
288
+ "id": timestamp,
289
+ "prompt": prompt,
290
+ "files": file_names,
291
+ "response": ai_response_text,
292
+ }
293
+ updated_history = (history_state_list or []) + [new_assessment]
294
+ history_labels = [f"{item['id']} - {item['prompt'][:40]}..." for item in updated_history]
295
+ yield final_chat, updated_history, gr.update(choices=history_labels)
296
+
297
+ def view_history(selection, history_state_list):
298
+ if not selection or not history_state_list:
299
+ return ""
300
+ selected_id = selection.split(" - ")[0]
301
+ selected_assessment = next((item for item in history_state_list if item["id"] == selected_id), None)
302
+ if selected_assessment:
303
+ file_list_md = "\n- ".join(selected_assessment["files"])
304
+ return (
305
+ f"### Assessment from: {selected_assessment['id']}\n"
306
+ f"**Files Used:**\n- {file_list_md}\n---\n"
307
+ f"**Original Prompt:**\n> {selected_assessment['prompt']}\n---\n"
308
+ f"**AI Generated Response:**\n{selected_assessment['response']}"
309
+ )
310
+ return "Could not find the selected assessment."
311
+
312
+ # Wire events
313
+ send_btn.click(
314
+ run_analysis_wrapper,
315
+ inputs=[prompt_input, files_input, chat_history_output, assessment_history],
316
+ outputs=[chat_history_output, assessment_history, history_dropdown],
317
+ )
318
+ history_dropdown.change(
319
+ view_history,
320
+ inputs=[history_dropdown, assessment_history],
321
+ outputs=[history_display],
322
+ )
323
+ clear_btn.click(lambda: (None, None, [], []),
324
+ outputs=[prompt_input, files_input, chat_history_output, assessment_history])
325
+ ping_btn.click(ping_cohere, outputs=[ping_out])
326
+ privacy_link.click(lambda: gr.update(visible=True), outputs=[privacy_modal])
327
+ close_privacy_btn.click(lambda: gr.update(visible=False), outputs=[privacy_modal])
328
+ terms_link.click(lambda: gr.update(visible=True), outputs=[terms_modal])
329
+ close_terms_btn.click(lambda: gr.update(visible=False), outputs=[terms_modal])
330
+
331
+
332
+ # =========================
333
+ # Entrypoint
334
+ # =========================
335
+
336
+ if __name__ == "__main__":
337
+ if not os.getenv("COHERE_API_KEY"):
338
+ print("🔴 COHERE_API_KEY environment variable not set. The app may not function correctly.")
339
+ demo.launch(server_name="0.0.0.0", server_port=int(os.getenv("PORT", "7860")))