GuglielmoTor commited on
Commit
c58585a
·
verified ·
1 Parent(s): 5f0b7f9

Update services/report_data_handler.py

Browse files
Files changed (1) hide show
  1. services/report_data_handler.py +63 -412
services/report_data_handler.py CHANGED
@@ -1,446 +1,102 @@
1
  # services/report_data_handler.py
 
 
 
 
 
2
  import pandas as pd
3
  import logging
4
- from apis.Bubble_API_Calls import fetch_linkedin_posts_data_from_bubble, bulk_upload_to_bubble
 
 
 
5
  from config import (
6
  BUBBLE_REPORT_TABLE_NAME,
7
  BUBBLE_OKR_TABLE_NAME,
8
  BUBBLE_KEY_RESULTS_TABLE_NAME,
9
- BUBBLE_TASKS_TABLE_NAME,
10
- BUBBLE_KR_UPDATE_TABLE_NAME,
11
  )
12
- import json # For handling JSON data
13
- from typing import List, Dict, Any, Optional, Tuple
14
- from datetime import date
15
 
16
- # It's good practice to configure the logger at the application entry point,
17
- # but setting a default handler here prevents "No handler found" warnings.
18
  logging.basicConfig(level=logging.INFO)
19
  logger = logging.getLogger(__name__)
20
 
21
  def fetch_latest_agentic_analysis(org_urn: str) -> Tuple[Optional[pd.DataFrame], Optional[str]]:
22
  """
23
- Fetches all agentic analysis data for a given org_urn from Bubble.
24
- Returns the full dataframe and any error message, or None, None.
25
  """
26
- logger.info(f"Starting fetch_latest_agentic_analysis for org_urn: {org_urn}")
27
-
28
- today = date.today()
29
- current_year = today.year
30
- current_quarter = (today.month - 1) // 3 + 1
31
-
32
  if not org_urn:
33
  logger.warning("fetch_latest_agentic_analysis: org_urn is missing.")
34
  return None, "org_urn is missing."
35
 
36
- additional_constraint = [
37
- {"key": 'quarter', "constraint_type": "equals", "value": current_quarter},
38
- {"key": 'year', "constraint_type": "equals", "value": current_year}
39
- ]
40
  try:
 
 
41
  report_data_df, error = fetch_linkedin_posts_data_from_bubble(
42
  data_type=BUBBLE_REPORT_TABLE_NAME,
43
  constraint_value=org_urn,
44
  constraint_key='organization_urn',
45
- constraint_type = 'equals'
46
  )
47
 
48
  if error:
49
- logger.error(f"Error fetching data from Bubble for org_urn {org_urn}: {error}")
50
  return None, str(error)
51
 
52
  if report_data_df is None or report_data_df.empty:
53
  logger.info(f"No existing agentic analysis found in Bubble for org_urn {org_urn}.")
54
- return None, None
55
 
56
- logger.info(f"Successfully fetched {len(report_data_df)} records for org_urn {org_urn}")
57
- return report_data_df, None # Return full dataframe and no error
58
 
59
  except Exception as e:
60
  logger.exception(f"An unexpected error occurred in fetch_latest_agentic_analysis for org_urn {org_urn}: {e}")
61
  return None, str(e)
62
 
63
- def _get_metrics_for_subject(data_subject: Optional[str], metrics: Dict[str, Any]) -> Optional[Dict[str, Any]]:
64
- """
65
- Selects the appropriate metrics dictionary from the main metrics object
66
- based on the key result's data_subject.
67
-
68
- Args:
69
- data_subject: The data subject string from the key result (e.g., 'follower_stats').
70
- metrics: The dictionary containing all available metrics for the agents.
71
-
72
- Returns:
73
- The relevant metrics dictionary, or None if no match is found.
74
- """
75
- if not data_subject or not metrics:
76
- return None
77
-
78
- # This mapping connects the data_subject string values to the keys in the `metrics` parameter.
79
- METRICS_KEY_MAPPING = {
80
- "follower_stats": "follower_agent",
81
- "posts": "post_agent",
82
- "mentions": "mentions_agent",
83
- }
84
-
85
- # Find the corresponding key for the metrics dictionary (e.g., 'follower_agent').
86
- metrics_key = METRICS_KEY_MAPPING.get(data_subject)
87
-
88
- if not metrics_key:
89
- logger.debug(f"No metrics mapping found for data_subject: '{data_subject}'")
90
- return None
91
-
92
- # Retrieve and return the actual metrics data using the resolved key.
93
- return metrics.get(metrics_key)
94
-
95
- def save_report_results(
96
- org_urn: str,
97
- report_markdown: str,
98
- quarter: int,
99
- year: int,
100
- report_type: str,
101
- ) -> Optional[str]:
102
- """Saves the agentic pipeline results to Bubble. Returns the new record ID or None."""
103
- logger.info(f"Starting save_report_results for org_urn: {org_urn}")
104
- if not org_urn:
105
- logger.error("Cannot save agentic results: org_urn is missing.")
106
- return None
107
-
108
- try:
109
- payload = {
110
- "organization_urn": org_urn,
111
- "report_text": report_markdown if report_markdown else "N/A",
112
- "quarter": quarter,
113
- "year": year,
114
- "report_type": report_type,
115
- }
116
- logger.info(f"Attempting to save agentic analysis to Bubble for org_urn: {org_urn}")
117
- response = bulk_upload_to_bubble([payload], BUBBLE_REPORT_TABLE_NAME)
118
-
119
- # Handle API response which could be a list of dicts (for bulk) or a single dict.
120
- if response and isinstance(response, list) and len(response) > 0 and isinstance(response[0], dict) and 'id' in response[0]:
121
- record_id = response[0]['id'] # Get the ID from the first dictionary in the list
122
- logger.info(f"Successfully saved agentic analysis to Bubble. Record ID: {record_id}")
123
- return record_id
124
- elif response and isinstance(response, dict) and "id" in response: # Handle non-bulk response
125
- record_id = response["id"]
126
- logger.info(f"Successfully saved agentic analysis to Bubble. Record ID: {record_id}")
127
- return record_id
128
- else:
129
- # Catches None, False, empty lists, or other unexpected formats.
130
- logger.error(f"Failed to save agentic analysis to Bubble. Unexpected API Response: {response}")
131
- return None
132
-
133
- except Exception as e:
134
- logger.exception(f"An unexpected error occurred in save_report_results for org_urn {org_urn}: {e}")
135
- return None
136
-
137
-
138
- # --- Data Saving Functions ---
139
-
140
- def save_objectives(
141
- org_urn: str,
142
- report_id: str,
143
- objectives_data: List[Dict[str, Any]]
144
- ) -> Optional[List[str]]:
145
- """
146
- Saves Objective records to Bubble.
147
- Returns a list of the newly created Bubble record IDs for the objectives, or None on failure.
148
- """
149
- logger.info(f"Starting save_objectives for report_id: {report_id}")
150
- if not objectives_data:
151
- logger.info("No objectives to save.")
152
- return []
153
-
154
- try:
155
- payloads = []
156
- for obj in objectives_data:
157
- timeline = obj.get("objective_timeline")
158
- payloads.append({
159
- "description": obj.get("objective_description"),
160
- # FIX: Convert Enum to its value before sending.
161
- "timeline": timeline.value if hasattr(timeline, 'value') else timeline,
162
- "owner": obj.get("objective_owner"),
163
- "report": report_id,
164
- })
165
-
166
- logger.info(f"Attempting to save {payloads} objectives for report_id: {report_id}")
167
- response_data = bulk_upload_to_bubble(payloads, BUBBLE_OKR_TABLE_NAME)
168
-
169
- # Validate response and extract IDs from the list of dictionaries.
170
- if not response_data or not isinstance(response_data, list):
171
- logger.error(f"Failed to save objectives. API response was not a list: {response_data}")
172
- return None
173
-
174
- try:
175
- # Extract the ID from each dictionary in the response list.
176
- extracted_ids = [item['id'] for item in response_data]
177
- except (TypeError, KeyError):
178
- logger.error(f"Failed to parse IDs from API response. Response format invalid: {response_data}", exc_info=True)
179
- return None
180
-
181
- # Check if we extracted the expected number of IDs
182
- if len(extracted_ids) != len(payloads):
183
- logger.error(f"Failed to save all objectives for report_id: {report_id}. "
184
- f"Expected {len(payloads)} IDs, but got {len(extracted_ids)} from response: {response_data}")
185
- return None
186
-
187
- logger.info(f"Successfully saved {len(extracted_ids)} objectives.")
188
- return extracted_ids
189
-
190
- except Exception as e:
191
- logger.exception(f"An unexpected error occurred in save_objectives for report_id {report_id}: {e}")
192
- return None
193
-
194
-
195
- def save_key_results(
196
- org_urn: str,
197
- objectives_with_ids: List[Tuple[Dict[str, Any], str]],
198
- metrics
199
- ) -> Optional[List[Tuple[Dict[str, Any], str]]]:
200
- """
201
- Saves Key Result records to Bubble, linking them to their parent objectives.
202
- Returns a list of tuples containing the original key result data and its new Bubble ID, or None on failure.
203
- """
204
- logger.info(f"Starting save_key_results for {len(objectives_with_ids)} objectives.")
205
- key_result_payloads = []
206
- # This list preserves the original KR data in the correct order to match the returned IDs
207
- key_results_to_process = []
208
-
209
- if not objectives_with_ids:
210
- logger.info("No objectives provided to save_key_results.")
211
- return []
212
-
213
- try:
214
- for objective_data, parent_objective_id in objectives_with_ids:
215
- # Defensive check to ensure the parent_objective_id is a valid-looking string.
216
- if not isinstance(parent_objective_id, str) or not parent_objective_id:
217
- logger.error(f"Invalid parent_objective_id found: '{parent_objective_id}'. Skipping KRs for this objective.")
218
- continue # Skip this loop iteration
219
-
220
- for kr in objective_data.get("key_results", []):
221
- kr_type = kr.get("key_result_type")
222
- key_results_to_process.append(kr)
223
- data_subject_value = kr.get("data_subject")
224
-
225
- metrics_for_kr = _get_metrics_for_subject(data_subject_value, metrics)
226
- metrics_as_string = json.dumps(metrics_for_kr) if metrics_for_kr else None
227
-
228
- key_result_payloads.append({
229
- "okr": parent_objective_id,
230
- "description": kr.get("key_result_description"),
231
- "target_metric": kr.get("target_metric"),
232
- "target_value": kr.get("target_value"),
233
- # FIX: Convert Enum to its value before sending.
234
- "kr_type": kr_type.value if hasattr(kr_type, 'value') else kr_type,
235
- "data_subject": kr.get("data_subject"),
236
- "metriche_data_subject": metrics_as_string
237
- })
238
-
239
- if not key_result_payloads:
240
- logger.info("No key results to save.")
241
- return []
242
-
243
- logger.info(f"Attempting to save {key_result_payloads} key results for org_urn: {org_urn}")
244
- response_data = bulk_upload_to_bubble(key_result_payloads, BUBBLE_KEY_RESULTS_TABLE_NAME)
245
-
246
- # Validate response and extract IDs.
247
- if not response_data or not isinstance(response_data, list):
248
- logger.error(f"Failed to save key results. API response was not a list: {response_data}")
249
- return None
250
-
251
- try:
252
- extracted_ids = [item['id'] for item in response_data]
253
- except (TypeError, KeyError):
254
- logger.error(f"Failed to parse IDs from key result API response: {response_data}", exc_info=True)
255
- return None
256
-
257
- if len(extracted_ids) != len(key_result_payloads):
258
- logger.error(f"Failed to save all key results for org_urn: {org_urn}. "
259
- f"Expected {len(key_result_payloads)} IDs, but got {len(extracted_ids)} from response: {response_data}")
260
- return None
261
-
262
- logger.info(f"Successfully saved {len(extracted_ids)} key results.")
263
- return list(zip(key_results_to_process, extracted_ids))
264
-
265
- except Exception as e:
266
- logger.exception(f"An unexpected error occurred in save_key_results for org_urn {org_urn}: {e}")
267
- return None
268
-
269
-
270
- def save_tasks(
271
- org_urn: str,
272
- key_results_with_ids: List[Tuple[Dict[str, Any], str]]
273
- ) -> Optional[List[str]]:
274
- """
275
- Saves Task records to Bubble, linking them to their parent key results.
276
- Returns a list of the newly created Bubble record IDs for the tasks, or None on failure.
277
- """
278
- logger.info(f"Starting save_tasks for {len(key_results_with_ids)} key results.")
279
- if not key_results_with_ids:
280
- logger.info("No key results provided to save_tasks.")
281
- return []
282
-
283
- try:
284
- task_payloads = []
285
- for key_result_data, parent_key_result_id in key_results_with_ids:
286
- for task in key_result_data.get("tasks", []):
287
- priority = task.get("priority")
288
- effort = task.get("effort")
289
- timeline = task.get("timeline")
290
- task_payloads.append({
291
- "key_result": parent_key_result_id,
292
- "description": task.get("task_description"),
293
- "deliverable": task.get("objective_deliverable"),
294
- "category": task.get("task_category"),
295
- # FIX: Convert Enum to its value before sending.
296
- "priority": priority.value if hasattr(priority, 'value') else priority,
297
- "priority_justification": task.get("priority_justification"),
298
- "effort": effort.value if hasattr(effort, 'value') else effort,
299
- "timeline": timeline.value if hasattr(timeline, 'value') else timeline,
300
- "responsible_party": task.get("responsible_party"),
301
- "success_criteria_metrics": task.get("success_criteria_metrics"),
302
- "dependencies": task.get("dependencies_prerequisites"),
303
- "why": task.get("why_proposed"),
304
- })
305
-
306
-
307
- if not task_payloads:
308
- logger.info("No tasks to save.")
309
- return []
310
-
311
- logger.info(f"Attempting to save {task_payloads} tasks for org_urn: {org_urn}")
312
- response_data = bulk_upload_to_bubble(task_payloads, BUBBLE_TASKS_TABLE_NAME)
313
-
314
- # Validate response and extract IDs.
315
- if not response_data or not isinstance(response_data, list):
316
- logger.error(f"Failed to save tasks. API response was not a list: {response_data}")
317
- return None
318
-
319
- try:
320
- extracted_ids = [item['id'] for item in response_data]
321
- except (TypeError, KeyError):
322
- logger.error(f"Failed to parse IDs from task API response: {response_data}", exc_info=True)
323
- return None
324
-
325
- if len(extracted_ids) != len(task_payloads):
326
- logger.error(f"Failed to save all tasks for org_urn: {org_urn}. "
327
- f"Expected {len(task_payloads)} IDs, but got {len(extracted_ids)} from response: {response_data}")
328
- return None
329
-
330
- logger.info(f"Successfully saved {len(extracted_ids)} tasks.")
331
- return extracted_ids
332
-
333
- except Exception as e:
334
- logger.exception(f"An unexpected error occurred in save_tasks for org_urn {org_urn}: {e}")
335
- return None
336
-
337
-
338
- # --- Orchestrator Function ---
339
-
340
- def save_actionable_okrs(org_urn: str, actionable_okrs: Dict[str, Any], report_id: str, metrics):
341
- """
342
- Orchestrates the sequential saving of objectives, key results, and tasks.
343
- """
344
- logger.info(f"--- Starting OKR save process for org_urn: {org_urn}, report_id: {report_id} ---")
345
-
346
- try:
347
- objectives_data = actionable_okrs.get("okrs", [])
348
-
349
- # Defensive check: If data is a string, try to parse it as JSON.
350
- if isinstance(objectives_data, str):
351
- logger.warning("The 'okrs' data is a string. Attempting to parse as JSON.")
352
- try:
353
- objectives_data = json.loads(objectives_data)
354
- logger.info("Successfully parsed 'okrs' data from JSON string.")
355
- except json.JSONDecodeError:
356
- logger.error("Failed to parse 'okrs' data. The string is not valid JSON.", exc_info=True)
357
- return # Abort if data is malformed
358
-
359
- if not objectives_data:
360
- logger.warning(f"No OKRs found in the input for org_urn: {org_urn}. Aborting save process.")
361
- return
362
-
363
- # Step 1: Save the top-level objectives
364
- objective_ids = save_objectives(org_urn, report_id, objectives_data)
365
- if objective_ids is None:
366
- logger.error("OKR save process aborted due to failure in saving objectives.")
367
- return
368
-
369
- # Combine the original objective data with their new IDs for the next step
370
- objectives_with_ids = list(zip(objectives_data, objective_ids))
371
-
372
- # Step 2: Save the key results, linking them to the objectives
373
- key_results_with_ids = save_key_results(org_urn, objectives_with_ids, metrics)
374
- if key_results_with_ids is None:
375
- logger.error("OKR save process aborted due to failure in saving key results.")
376
- return
377
-
378
- # Step 3: Save the tasks, linking them to the key results
379
- task_ids = save_tasks(org_urn, key_results_with_ids)
380
- if task_ids is None:
381
- logger.error("Task saving failed, but objectives and key results were saved.")
382
- # For now, we just log the error and complete.
383
- return
384
-
385
- logger.info(f"--- OKR save process completed successfully for org_urn: {org_urn} ---")
386
-
387
- except Exception as e:
388
- logger.exception(f"An unhandled exception occurred during the save_actionable_okrs orchestration for org_urn {org_urn}: {e}")
389
-
390
 
391
  def fetch_and_reconstruct_data_from_bubble(report_df: pd.DataFrame) -> Optional[Dict[str, Any]]:
392
  """
393
- Fetches the latest report, OKRs, Key Results, and Tasks from Bubble for a given organization
394
- and reconstructs them into the nested structure expected by the application.
395
 
396
  Args:
397
- org_urn: The URN of the organization.
398
 
399
  Returns:
400
- A dictionary containing the reconstructed data ('report_str', 'actionable_okrs', etc.)
401
- or None if the report is not found or an error occurs.
402
  """
403
- # logger.info(f"Starting data fetch and reconstruction for org_urn: {org_urn}")
404
- # try:
405
- # # 1. Fetch the latest report for the organization
406
- # # We add a sort field to get the most recent one.
407
- # report_df, error = fetch_linkedin_posts_data_from_bubble(
408
- # data_type=BUBBLE_REPORT_TABLE_NAME,
409
- # org_urn=org_urn,
410
- # constraint_key="organization_urn"
411
- # )
412
-
413
- # if error or report_df is None or report_df.empty:
414
- # logger.error(f"Could not fetch latest report for org_urn {org_urn}. Error: {error}")
415
- # return None
416
 
417
- logger.info(f"Starting data fetch and reconstruction")
418
  try:
419
- # Get the most recent report (assuming the first one is the latest)
420
- latest_report = report_df.iloc[0]
 
421
  report_id = latest_report.get('_id')
422
  if not report_id:
423
- logger.error("Fetched report is missing a Bubble '_id'.")
424
  return None
425
-
426
- logger.info(f"Fetched latest report with ID: {report_id}")
427
 
428
- # 2. Fetch all related OKRs using the report_id
 
 
429
  okrs_df, error = fetch_linkedin_posts_data_from_bubble(
430
  data_type=BUBBLE_OKR_TABLE_NAME,
431
- constraint_value=str(report_id),
432
  constraint_key='report',
433
- constraint_type = 'equals'
434
  )
435
  if error:
436
  logger.error(f"Error fetching OKRs for report_id {report_id}: {error}")
437
- okrs_df = pd.DataFrame()
438
 
439
-
440
- logger.info(f" okr_df {okrs_df}")
441
- # 3. Fetch all related Key Results using the OKR IDs
442
  okr_ids = okrs_df['_id'].tolist() if not okrs_df.empty else []
443
- logger.info(f" retrieved {len(okr_ids)} okr ID: {okr_ids}")
444
  krs_df = pd.DataFrame()
445
  if okr_ids:
446
  krs_df, error = fetch_linkedin_posts_data_from_bubble(
@@ -449,11 +105,9 @@ def fetch_and_reconstruct_data_from_bubble(report_df: pd.DataFrame) -> Optional[
449
  constraint_key='okr',
450
  constraint_type='in'
451
  )
452
- if error:
453
- logger.error(f"Error fetching Key Results for OKR IDs {okr_ids}: {error}")
454
- krs_df = pd.DataFrame()
455
 
456
- # 4. Fetch all related Tasks using the Key Result IDs
457
  kr_ids = krs_df['_id'].tolist() if not krs_df.empty else []
458
  tasks_df = pd.DataFrame()
459
  if kr_ids:
@@ -463,38 +117,35 @@ def fetch_and_reconstruct_data_from_bubble(report_df: pd.DataFrame) -> Optional[
463
  constraint_key='key_result',
464
  constraint_type='in'
465
  )
466
- if error:
467
- logger.error(f"Error fetching Tasks for KR IDs {kr_ids}: {error}")
468
- tasks_df = pd.DataFrame()
469
 
470
- # 5. Reconstruct the nested 'actionable_okrs' dictionary
471
- tasks_by_kr_id = tasks_df.groupby('key_result').apply(lambda x: x.to_dict('records')).to_dict()
472
- krs_by_okr_id = krs_df.groupby('okr').apply(lambda x: x.to_dict('records')).to_dict()
473
 
474
  reconstructed_okrs = []
475
- for okr_data in okrs_df.to_dict('records'):
476
- okr_id = okr_data['_id']
477
- key_results_list = krs_by_okr_id.get(okr_id, [])
478
-
479
- for kr_data in key_results_list:
480
- kr_id = kr_data['_id']
481
- # Attach tasks to each key result
482
- kr_data['tasks'] = tasks_by_kr_id.get(kr_id, [])
483
-
484
- # Attach key results to the objective
485
- okr_data['key_results'] = key_results_list
486
- reconstructed_okrs.append(okr_data)
487
-
488
  actionable_okrs = {"okrs": reconstructed_okrs}
489
-
490
- return {
491
- "report_str": latest_report.get("report_text", "Nessun report trovato."),
492
  "quarter": latest_report.get("quarter"),
493
  "year": latest_report.get("year"),
494
  "actionable_okrs": actionable_okrs,
495
  "report_id": report_id
496
  }
 
 
497
 
498
  except Exception as e:
499
- logger.exception(f"An unexpected error occurred during data reconstruction for org_urn {org_urn}: {e}")
500
- return None
 
1
  # services/report_data_handler.py
2
+ """
3
+ This module is responsible for fetching pre-computed agentic analysis data
4
+ (reports, OKRs, etc.) from Bubble.io and reconstructing it into a nested
5
+ dictionary format that the Gradio UI can easily display.
6
+ """
7
  import pandas as pd
8
  import logging
9
+ from typing import Dict, Any, Optional, Tuple
10
+
11
+ # This is the only function needed from the Bubble API module for this handler
12
+ from apis.Bubble_API_Calls import fetch_linkedin_posts_data_from_bubble
13
  from config import (
14
  BUBBLE_REPORT_TABLE_NAME,
15
  BUBBLE_OKR_TABLE_NAME,
16
  BUBBLE_KEY_RESULTS_TABLE_NAME,
17
+ BUBBLE_TASKS_TABLE_NAME
 
18
  )
 
 
 
19
 
 
 
20
  logging.basicConfig(level=logging.INFO)
21
  logger = logging.getLogger(__name__)
22
 
23
  def fetch_latest_agentic_analysis(org_urn: str) -> Tuple[Optional[pd.DataFrame], Optional[str]]:
24
  """
25
+ Fetches all agentic analysis report data for a given org_urn from Bubble.
26
+ This function is called once during the initial data load.
27
  """
28
+ logger.info(f"Fetching latest agentic analysis data from Bubble for org_urn: {org_urn}")
 
 
 
 
 
29
  if not org_urn:
30
  logger.warning("fetch_latest_agentic_analysis: org_urn is missing.")
31
  return None, "org_urn is missing."
32
 
 
 
 
 
33
  try:
34
+ # We fetch all reports and will sort them later if needed, but typically the
35
+ # external process should manage providing the "latest" or "active" report.
36
  report_data_df, error = fetch_linkedin_posts_data_from_bubble(
37
  data_type=BUBBLE_REPORT_TABLE_NAME,
38
  constraint_value=org_urn,
39
  constraint_key='organization_urn',
40
+ constraint_type='equals'
41
  )
42
 
43
  if error:
44
+ logger.error(f"Error fetching agentic reports from Bubble for org_urn {org_urn}: {error}")
45
  return None, str(error)
46
 
47
  if report_data_df is None or report_data_df.empty:
48
  logger.info(f"No existing agentic analysis found in Bubble for org_urn {org_urn}.")
49
+ return pd.DataFrame(), None # Return empty DataFrame, no error
50
 
51
+ logger.info(f"Successfully fetched {len(report_data_df)} agentic report records for org_urn {org_urn}")
52
+ return report_data_df, None
53
 
54
  except Exception as e:
55
  logger.exception(f"An unexpected error occurred in fetch_latest_agentic_analysis for org_urn {org_urn}: {e}")
56
  return None, str(e)
57
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
 
59
  def fetch_and_reconstruct_data_from_bubble(report_df: pd.DataFrame) -> Optional[Dict[str, Any]]:
60
  """
61
+ Takes a DataFrame of report data, fetches all related child items (OKRs, KRs, Tasks)
62
+ from Bubble, and reconstructs the full nested dictionary expected by the UI.
63
 
64
  Args:
65
+ report_df: The DataFrame containing one or more reports, fetched previously.
66
 
67
  Returns:
68
+ A dictionary containing the reconstructed data ('report_str', 'actionable_okrs'),
69
+ or None if the report is not found or a critical error occurs.
70
  """
71
+ logger.info("Starting data reconstruction from fetched Bubble data.")
72
+ if report_df is None or report_df.empty:
73
+ logger.warning("Cannot reconstruct data, the provided report DataFrame is empty.")
74
+ return None
 
 
 
 
 
 
 
 
 
75
 
 
76
  try:
77
+ # Assuming the most recent report is desired if multiple are returned.
78
+ # You might need more sophisticated logic here to select the "active" report.
79
+ latest_report = report_df.sort_values(by='Created Date', ascending=False).iloc[0]
80
  report_id = latest_report.get('_id')
81
  if not report_id:
82
+ logger.error("Fetched report is missing a Bubble '_id', cannot reconstruct children.")
83
  return None
 
 
84
 
85
+ logger.info(f"Reconstructing data for the latest report, ID: {report_id}")
86
+
87
+ # 1. Fetch all related OKRs using the report_id
88
  okrs_df, error = fetch_linkedin_posts_data_from_bubble(
89
  data_type=BUBBLE_OKR_TABLE_NAME,
90
+ constraint_value=report_id,
91
  constraint_key='report',
92
+ constraint_type='equals'
93
  )
94
  if error:
95
  logger.error(f"Error fetching OKRs for report_id {report_id}: {error}")
96
+ return None # Fail reconstruction if children can't be fetched
97
 
98
+ # 2. Fetch all related Key Results using the OKR IDs
 
 
99
  okr_ids = okrs_df['_id'].tolist() if not okrs_df.empty else []
 
100
  krs_df = pd.DataFrame()
101
  if okr_ids:
102
  krs_df, error = fetch_linkedin_posts_data_from_bubble(
 
105
  constraint_key='okr',
106
  constraint_type='in'
107
  )
108
+ if error: logger.error(f"Error fetching Key Results: {error}")
 
 
109
 
110
+ # 3. Fetch all related Tasks using the Key Result IDs
111
  kr_ids = krs_df['_id'].tolist() if not krs_df.empty else []
112
  tasks_df = pd.DataFrame()
113
  if kr_ids:
 
117
  constraint_key='key_result',
118
  constraint_type='in'
119
  )
120
+ if error: logger.error(f"Error fetching Tasks: {error}")
 
 
121
 
122
+ # 4. Reconstruct the nested dictionary
123
+ tasks_by_kr_id = tasks_df.groupby('key_result').apply(lambda x: x.to_dict('records')).to_dict() if not tasks_df.empty else {}
124
+ krs_by_okr_id = krs_df.groupby('okr').apply(lambda x: x.to_dict('records')).to_dict() if not krs_df.empty else {}
125
 
126
  reconstructed_okrs = []
127
+ if not okrs_df.empty:
128
+ for okr_data in okrs_df.to_dict('records'):
129
+ okr_id = okr_data['_id']
130
+ key_results_list = krs_by_okr_id.get(okr_id, [])
131
+ for kr_data in key_results_list:
132
+ kr_id = kr_data['_id']
133
+ kr_data['tasks'] = tasks_by_kr_id.get(kr_id, [])
134
+ okr_data['key_results'] = key_results_list
135
+ reconstructed_okrs.append(okr_data)
136
+
137
+ # 5. Assemble the final payload for the UI
 
 
138
  actionable_okrs = {"okrs": reconstructed_okrs}
139
+ final_reconstructed_data = {
140
+ "report_str": latest_report.get("report_text", "Report text not found."),
 
141
  "quarter": latest_report.get("quarter"),
142
  "year": latest_report.get("year"),
143
  "actionable_okrs": actionable_okrs,
144
  "report_id": report_id
145
  }
146
+ logger.info("Successfully reconstructed nested data structure for the UI.")
147
+ return final_reconstructed_data
148
 
149
  except Exception as e:
150
+ logger.exception(f"An unexpected error occurred during data reconstruction: {e}")
151
+ return None