|
4 | 4 | This module provides functionality to load templates from the database. |
5 | 5 | """ |
6 | 6 |
|
| 7 | +import asyncio |
| 8 | +import hashlib |
| 9 | +import json |
7 | 10 | from typing import Dict, Optional, Tuple |
8 | 11 |
|
9 | 12 | from app.ai.voice.agents.breeze_buddy.template.transformation_function import ( |
10 | 13 | TEMPLATE_FUNCTION_REGISTRY, |
11 | 14 | ) |
12 | 15 | from app.ai.voice.agents.breeze_buddy.template.types import ( |
| 16 | + DataSourceRef, |
13 | 17 | FlowMode, |
14 | 18 | TemplateModel, |
15 | 19 | ) |
|
21 | 25 | from app.database.accessor.breeze_buddy.template import ( |
22 | 26 | get_template_by_id_with_fallback, |
23 | 27 | ) |
| 28 | +from app.services.data_sources import DATA_SOURCE_UNAVAILABLE |
| 29 | +from app.services.google.sheets import extract_spreadsheet_id, fetch_formatted |
| 30 | +from app.services.redis import get_redis_service |
24 | 31 |
|
25 | 32 |
|
26 | 33 | class FlowConfigLoader: |
@@ -205,6 +212,23 @@ async def load_template( |
205 | 212 | f"Injected extra payload field '{field_name}' into template_vars" |
206 | 213 | ) |
207 | 214 |
|
| 215 | + # 5. data sources — inject as {datasource_<name>} variables |
| 216 | + if template_obj.data_sources: |
| 217 | + contents = await asyncio.gather( |
| 218 | + *[ |
| 219 | + self._fetch_one_data_source(ref) |
| 220 | + for ref in template_obj.data_sources |
| 221 | + ], |
| 222 | + return_exceptions=True, |
| 223 | + ) |
| 224 | + for ref, result in zip(template_obj.data_sources, contents): |
| 225 | + if not isinstance(result, Exception) and result is not None: |
| 226 | + var_key = f"datasource_{ref.name}" |
| 227 | + if var_key not in template_vars: |
| 228 | + template_vars[var_key] = result |
| 229 | + logger.info( |
| 230 | + f"Injected data source '{ref.name}' as variable '{var_key}'" |
| 231 | + ) |
208 | 232 | # Direct mode has a flat structure (system_prompt + flat function list) |
209 | 233 | # rather than a nodes array, so render the top-level message fields and |
210 | 234 | # skip the per-node loop entirely. |
@@ -275,3 +299,59 @@ async def load_template( |
275 | 299 |
|
276 | 300 | logger.info(f"Rendered task messages for template {template_obj.name}") |
277 | 301 | return template_obj, template_vars |
| 302 | + |
| 303 | + async def _fetch_one_data_source(self, ref: "DataSourceRef") -> str: |
| 304 | + if not ref.is_active: |
| 305 | + return DATA_SOURCE_UNAVAILABLE |
| 306 | + |
| 307 | + spreadsheet_id = extract_spreadsheet_id(ref.spreadsheet_url) |
| 308 | + if not spreadsheet_id: |
| 309 | + logger.warning( |
| 310 | + "Data source '%s' has malformed spreadsheet_url: %s", |
| 311 | + ref.name, |
| 312 | + ref.spreadsheet_url, |
| 313 | + ) |
| 314 | + return DATA_SOURCE_UNAVAILABLE |
| 315 | + |
| 316 | + sheet = ref.sheet_name or "_first_" |
| 317 | + cols = json.dumps(sorted(ref.columns or [])) |
| 318 | + cols_digest = hashlib.md5(cols.encode()).hexdigest()[:8] |
| 319 | + cache_key = ( |
| 320 | + f"datasource:{spreadsheet_id}:{sheet}:{cols_digest}:{ref.format.value}" |
| 321 | + ) |
| 322 | + |
| 323 | + try: |
| 324 | + redis = await get_redis_service() |
| 325 | + cached = await redis.get(cache_key) |
| 326 | + if cached: |
| 327 | + return cached |
| 328 | + except Exception: |
| 329 | + pass |
| 330 | + |
| 331 | + try: |
| 332 | + content = await asyncio.wait_for( |
| 333 | + fetch_formatted( |
| 334 | + spreadsheet_id=spreadsheet_id, |
| 335 | + sheet_name=ref.sheet_name, |
| 336 | + columns=ref.columns, |
| 337 | + format=ref.format.value, |
| 338 | + ), |
| 339 | + timeout=5.0, |
| 340 | + ) |
| 341 | + except asyncio.TimeoutError: |
| 342 | + logger.warning("Data source '%s' fetch timed out", ref.name) |
| 343 | + return DATA_SOURCE_UNAVAILABLE |
| 344 | + except Exception as exc: |
| 345 | + logger.error( |
| 346 | + "Data source '%s' fetch failed: %s", ref.name, exc, exc_info=True |
| 347 | + ) |
| 348 | + return DATA_SOURCE_UNAVAILABLE |
| 349 | + |
| 350 | + if content != DATA_SOURCE_UNAVAILABLE: |
| 351 | + try: |
| 352 | + redis = await get_redis_service() |
| 353 | + await redis.setex(cache_key, content, ttl_seconds=60) |
| 354 | + except Exception: |
| 355 | + pass |
| 356 | + |
| 357 | + return content |
0 commit comments