CoolFace
Apppublic

ani-sdhu/essp

sourceHugging Faceupdated 2mo agoView on Hugging Face
0likes
ai_command.py387 linesDownload Raw Back to root
1#!/usr/bin/env python32"""3ai_command.py4=============5 6A natural-language command bar for the energy/economic dashboard. You type7something like "compare henry hub price and US residential gas consumption" or8"add CPI" or "clear the chart", and it gets resolved to real series and9plotted.10 11How it stays reliable12---------------------13The language model does only the language understanding: it turns your sentence14into a small JSON plan (which items to plot, from which source, whether to15replace or add). It does NOT invent series IDs. The plan is then resolved by16the dashboard's existing deterministic search and fetch layer (FRED live17search and the EIA catalog index), so what actually gets plotted is always a18real series that the providers returned.19 20Provider21--------22Defaults to Groq, which exposes an OpenAI-compatible endpoint at23https://api.groq.com/openai/v1/chat/completions and a strict JSON mode via24response_format (Groq, 2026). Because it is OpenAI-compatible, you can point it25at any compatible endpoint by changing base_url and model.26 27Wiring into energy_data_dashboard.py28------------------------------------29In DashboardUI.__init__, after self.hub is set:30 31    from ai_command import LLMClient, CommandBar32    llm = LLMClient(33        api_key=read_key("GROQ_API_KEY", ".groq_key"),34        model=os.environ.get("GROQ_MODEL", "llama-3.3-70b-versatile"),35    )36    self.cmd = CommandBar(37        hub=self.hub,38        llm=llm,39        on_plot=self._ai_plot,          # see adapter below40        get_context=self._ai_context,   # see adapter below41    )42 43Add these two small adapters to DashboardUI:44 45    def _ai_plot(self, frames, replace):46        if replace:47            self.frames = list(frames)48        else:49            for df in frames:50                lbl = df["label"].iloc[0]51                self.frames = [f for f in self.frames52                               if f["label"].iloc[0] != lbl] + [df]53        self._render()54 55    def _ai_context(self):56        return {57            "plotted": [f["label"].iloc[0] for f in self.frames],58            "recent_results": [f"{r['source']}:{r.get('id','')}"59                               for r in getattr(self, "results", [])[:8]],60        }61 62Then drop self.cmd.panel() at the top of the main column in template().63 64Reference65---------66Groq. (2026). Text generation and structured outputs. GroqDocs.67    https://console.groq.com/docs/text-chat68"""69 70from __future__ import annotations71 72import json73import re74from typing import Any, Callable, Optional75 76import pandas as pd77import requests78import panel as pn79 80 81DEFAULT_BASE_URL = "https://api.groq.com/openai/v1"82DEFAULT_MODEL = "llama-3.3-70b-versatile"83 84PLAN_SCHEMA_HINT = """You translate a user's request about economic and energy85time series into a small JSON plan. Output ONLY a JSON object, no prose.86 87Sources:88- "FRED": macroeconomic and financial series (GDP, CPI, interest rates, broad89  price series such as the Henry Hub natural gas spot price, exchange rates).90- "EIA": United States energy statistics (natural gas, electricity, petroleum;91  often broken out by state and by sector such as residential or commercial).92- Use "any" only when you cannot tell.93 94JSON shape:95{96  "action": "plot" | "add" | "compare" | "clear",97  "items": [98    {99      "source": "FRED" | "EIA" | "any",100      "query": "short description used to search the source",101      "series_hint": "extra keyword to pick one EIA series, e.g. residential or Georgia",102      "exact_id": "a FRED series id only if you are confident, else empty"103    }104  ],105  "start": "optional start period, e.g. 1997-01 or 1997-01-01, else empty",106  "end": "optional end period, else empty"107}108 109Rules:110- "compare" and "plot" replace the chart with the listed items.111- "add" appends the listed items to whatever is already plotted.112- "clear" empties the chart and needs no items.113- Resolve references like "these two", "them", or "the second one" using the114  CONTEXT block (currently plotted series and recent search results). If the115  user says "compare these two" put the two referenced items in "items".116- For EIA energy series by sector or state, set source "EIA", put the dataset117  topic in "query" and the sector or state in "series_hint".118- Keep queries short and literal; do not invent identifiers.119 120Examples:121User: compare henry hub price and US residential natural gas consumption122{"action":"compare","items":[123  {"source":"FRED","query":"henry hub natural gas spot price","series_hint":"","exact_id":"DHHNGSP"},124  {"source":"EIA","query":"natural gas consumption","series_hint":"residential","exact_id":""}],125 "start":"","end":""}126 127User: add CPI since 2010128{"action":"add","items":[129  {"source":"FRED","query":"consumer price index all urban","series_hint":"","exact_id":"CPIAUCSL"}],130 "start":"2010-01-01","end":""}131 132User: clear it133{"action":"clear","items":[],"start":"","end":""}134"""135 136 137# ----------------------------------------------------------------------------138# LLM client (OpenAI-compatible; defaults to Groq). transport is injectable.139# ----------------------------------------------------------------------------140class LLMClient:141    def __init__(self, api_key: Optional[str], model: str = DEFAULT_MODEL,142                 base_url: str = DEFAULT_BASE_URL,143                 transport: Optional[Callable[[dict], dict]] = None):144        self.api_key = (api_key or "").strip()145        self.model = model146        self.base_url = base_url.rstrip("/")147        self._custom_transport = transport is not None148        self._transport = transport or self._http_post149 150    @property151    def available(self) -> bool:152        # Usable if we have a key for the real endpoint, or an injected transport.153        return bool(self.api_key) or self._custom_transport154 155    def _http_post(self, payload: dict) -> dict:156        r = requests.post(157            f"{self.base_url}/chat/completions",158            headers={"Authorization": f"Bearer {self.api_key}",159                     "Content-Type": "application/json"},160            json=payload, timeout=40)161        if r.status_code != 200:162            raise RuntimeError(f"LLM HTTP {r.status_code}: {r.text[:200]}")163        return r.json()164 165    def plan(self, user_text: str, context: dict) -> dict:166        ctx = json.dumps(context or {}, ensure_ascii=False)167        payload = {168            "model": self.model,169            "temperature": 0.1,170            "response_format": {"type": "json_object"},171            "messages": [172                {"role": "system", "content": PLAN_SCHEMA_HINT},173                {"role": "user",174                 "content": f"CONTEXT:\n{ctx}\n\nREQUEST:\n{user_text}"},175            ],176        }177        data = self._transport(payload)178        content = (data.get("choices", [{}])[0]179                   .get("message", {}).get("content", "")) or ""180        return self._parse_json(content)181 182    @staticmethod183    def _parse_json(text: str) -> dict:184        text = text.strip()185        if text.startswith("```"):186            text = re.sub(r"^```(?:json)?|```$", "", text, flags=re.MULTILINE).strip()187        try:188            return json.loads(text)189        except Exception:190            m = re.search(r"\{.*\}", text, flags=re.DOTALL)191            if m:192                return json.loads(m.group(0))193            raise194 195 196# ----------------------------------------------------------------------------197# Command bar: resolve a plan against the existing DataHub and plot it198# ----------------------------------------------------------------------------199class CommandBar:200    def __init__(self, hub, llm: LLMClient,201                 on_plot: Callable[[list, bool], None],202                 get_context: Callable[[], dict] = lambda: {}):203        self.hub = hub204        self.llm = llm205        self.on_plot = on_plot206        self.get_context = get_context207 208    # -- resolution --------------------------------------------------------209    def _resolve_fred(self, item: dict, start: str, end: str) -> Optional[pd.DataFrame]:210        if self.hub.fred is None:211            return None212        exact = (item.get("exact_id") or "").strip()213        query = item.get("query", "")214        try:215            hits = self.hub.fred.search(query or exact, limit=8)216        except Exception:217            hits = []218        top = None219        if exact:220            top = next((h for h in hits if (h.get("id") or "").upper() == exact.upper()), None)221            if top is None:222                # Not in search results: fetch by id with id as the fallback name.223                df = self.hub.fetch_fred(exact, exact, item.get("units", ""), start, end)224                return df if (df is not None and not df.empty) else None225        if top is None:226            top = hits[0] if hits else None227        if top is None:228            return None229        df = self.hub.fetch_fred(top["id"], top.get("title", ""),230                                 top.get("units", ""), start, end)231        return df if not df.empty else None232 233    def _resolve_eia(self, item: dict, start: str, end: str) -> Optional[pd.DataFrame]:234        if self.hub.eia is None:235            return None236        datasets = self.hub.eia_dataset_search(item.get("query", ""), limit=20)237        if not datasets:238            return None239        # Prefer a dataset that exposes individual series when a hint is given.240        ds = next((d for d in datasets if d.get("series_facet")), datasets[0])241        route = ds["route"]242        freq = ds.get("default_frequency") or (243            ds.get("frequencies", [None]) or [None])[0] or ""244        facet = ds.get("series_facet")245        if facet:246            hint = (item.get("series_hint") or item.get("query") or "").strip()247            found = self.hub.eia_series(route, hint)248            results = found.get("results", [])249            if not results and hint:250                results = self.hub.eia_series(route, "").get("results", [])251            if not results:252                return None253            sid = results[0]["id"]254            sname = results[0].get("name", "")255            df = self.hub.fetch_eia(route, facet, sid, freq, start, end,256                                    series_name=sname)257        else:258            df = self.hub.fetch_eia(route, None, "", freq, start, end,259                                    series_name=ds.get("name", ""))260        return df if not df.empty else None261 262    def _resolve(self, item: dict, start: str, end: str):263        src = (item.get("source") or "any").upper()264        if src == "FRED":265            return self._resolve_fred(item, start, end), "FRED"266        if src == "EIA":267            return self._resolve_eia(item, start, end), "EIA"268        # "any": try FRED first, then EIA.269        df = self._resolve_fred(item, start, end)270        if df is not None:271            return df, "FRED"272        return self._resolve_eia(item, start, end), "EIA"273 274    # -- execute -----------------------------------------------------------275    def execute(self, user_text: str) -> str:276        user_text = (user_text or "").strip()277        if not user_text:278            return "Type a request first."279        try:280            plan = self.llm.plan(user_text, self.get_context())281        except Exception as exc:282            return f"Could not reach the language model: {exc}"283 284        action = (plan.get("action") or "plot").lower()285        start = (plan.get("start") or "").strip()286        end = (plan.get("end") or "").strip()287        items = plan.get("items") or []288 289        if action == "clear":290            self.on_plot([], True)291            return "Cleared the chart."292 293        if not items:294            return ("I could not tell which series you meant. Try naming them, "295                    "for example: compare henry hub price and US residential "296                    "gas consumption.")297 298        frames, resolved, missed = [], [], []299        for it in items:300            try:301                df, _src = self._resolve(it, start, end)302            except Exception as exc:303                df = None304                missed.append(f"{it.get('query','?')} ({exc})")305            if df is not None and not df.empty:306                frames.append(df)307                resolved.append(df["label"].iloc[0])308            else:309                missed.append(it.get("query", "?"))310 311        if not frames:312            return "Nothing matched. " + (313                "Tried: " + "; ".join(missed) if missed else "")314 315        replace = action != "add"316        self.on_plot(frames, replace)317 318        verb = "Plotted" if replace else "Added"319        msg = f"{verb} {', '.join(resolved)}."320        if missed:321            msg += f" Could not resolve: {', '.join(missed)}."322        return msg323 324    # -- panel widget ------------------------------------------------------325    def _find_groq_key(self) -> Optional[str]:326        import os327        from pathlib import Path328        v = os.environ.get("GROQ_API_KEY")329        if v and v.strip():330            return v.strip()331        names = [".groq_key", "_groq_key", "key.groq_key", "groq_key.txt", "groq.key"]332        dirs = [Path.cwd(), Path(__file__).resolve().parent, Path.home()]333        for d in dirs:334            for nm in names:335                p = d / nm336                try:337                    if p.is_file():338                        t = p.read_text(encoding="utf-8").strip()339                        if t:340                            return t341                except Exception:342                    pass343        return None344 345    def panel(self) -> pn.Column:346        # Pull in a key from common locations if one was not passed in.347        if not self.llm.api_key:348            k = self._find_groq_key()349            if k:350                self.llm.api_key = k351 352        box = pn.widgets.TextInput(353            placeholder='Ask: "compare henry hub price and US residential gas '354                        'consumption", "add CPI", "clear it"',355            sizing_mode="stretch_width")356        btn = pn.widgets.Button(name="Ask", button_type="primary", width=90)357        status = pn.pane.Markdown("", styles={"color": "#8FA3B8",358                                              "font-size": "12px"})359 360        def run(_=None):361            text = (box.value or "").strip()362            if not text:363                status.object = "Type a request first."364                return365            if not self.llm.api_key:366                k = self._find_groq_key()367                if k:368                    self.llm.api_key = k369            if not self.llm.api_key:370                status.object = ("No Groq key found. Set GROQ_API_KEY, or save your "371                                 "key in a file named .groq_key or _groq_key next to "372                                 "the dashboard, then click Ask again.")373                return374            status.object = "Thinking..."375            status.object = self.execute(text)376 377        btn.on_click(run)378        box.param.watch(lambda e: run(), "value")  # Enter submits379 380        # The box and button are always enabled, so the bar is always clickable.381        # position/z-index guard against a stacking-context overlay stealing clicks.382        return pn.Column(383            pn.Row(box, btn), status,384            sizing_mode="stretch_width",385            styles={"position": "relative", "z-index": "20",386                    "pointer-events": "auto"})387