from __future__ import annotations import json import logging import os from pathlib import Path from datetime import datetime, timezone from typing import Any, Dict, Optional, Tuple from dotenv import load_dotenv def load_environment() -> None: """Load environment variables from a local .env if present.""" # Prefer the project-local .env at ai_business_automation_agent/.env. # This avoids surprises when Streamlit's working directory differs. project_env = Path(__file__).resolve().parent / ".env" # Use override=True to ensure .env values replace empty process env vars. if project_env.exists(): load_dotenv(dotenv_path=project_env, override=True) else: load_dotenv(override=True) def setup_logging() -> None: level = os.getenv("LOG_LEVEL", "INFO").upper().strip() logging.basicConfig( level=level, format="%(asctime)s | %(levelname)s | %(name)s | %(message)s", ) def utc_now_iso() -> str: return datetime.now(timezone.utc).isoformat() def _extract_first_json_object(text: str) -> Optional[str]: """Best-effort extraction of first top-level JSON object from text.""" start = text.find("{") if start == -1: return None depth = 0 for i in range(start, len(text)): ch = text[i] if ch == "{": depth += 1 elif ch == "}": depth -= 1 if depth == 0: return text[start : i + 1] return None def parse_llm_json(text: str) -> Tuple[Dict[str, Any], Optional[str]]: """ Parse strict JSON from an LLM response. Returns (obj, error). If parsing fails, obj will be {}, error will be a message. """ raw = text.strip() try: return json.loads(raw), None except Exception: candidate = _extract_first_json_object(raw) if not candidate: return {}, "No JSON object found in model output." try: return json.loads(candidate), None except Exception as e: return {}, f"Failed to parse JSON: {e}" def append_agent_log(state: Dict[str, Any], *, agent: str, event: str, payload: Any) -> Dict[str, Any]: logs = list(state.get("agent_logs") or []) logs.append( { "ts": utc_now_iso(), "agent": agent, "event": event, "payload": payload, } ) return {"agent_logs": logs}