CoolFace
Apppublic

BHAVIKBANKER/BERTopic_AGENTIC_AI__GROUP_1

sourceHugging Faceupdated 4mo agoView on Hugging Face
0likes
agent.py280 linesDownload Raw Back to root
1"""2agent.py — LangGraph-based topic analysis agent (§11).33-LLM Council for topic modelling, 4 sheets, triple-agreement tracking.4"""5from __future__ import annotations6import json, logging, os, re, time7from dataclasses import dataclass, field, asdict8from typing import TypedDict, Optional9from collections import Counter10import pandas as pd, numpy as np, requests11from groq import Groq12from langgraph.graph import StateGraph, END13 14logging.basicConfig(level=logging.INFO, format="%(levelname)s | %(message)s")15logger = logging.getLogger(__name__)16 17GROQ_MODEL = "llama-3.1-8b-instant"18MISTRAL_MODEL = "mistral-small-latest"19 20# ---------------------------------------------------------------------------21# LangGraph state22# ---------------------------------------------------------------------------23class PipelineState(TypedDict, total=False):24    filepath: str25    groq_key: str26    mistral_key: str27    gemini_key: str28    n_trials: int29    topic_data: dict30    interpretations: dict31    sheets: dict          # {1: [...], 2: [...], 3: [...], 4: [...]}32    agreement_rates: dict33    mismatch_table: list34    json_path: str35    csv_path: str36    error: str37 38# ---------------------------------------------------------------------------39# API helpers40# ---------------------------------------------------------------------------41def _parse(raw: str) -> dict:42    raw = raw.strip().replace("```json","").replace("```","").strip()43    s, e = raw.find("{"), raw.rfind("}")+144    if s != -1 and e > 0: raw = raw[s:e]45    try: return json.loads(raw)46    except: return {}47 48def _groq(client, prompt):49    try:50        r = client.chat.completions.create(model=GROQ_MODEL,51            messages=[{"role":"user","content":prompt}], temperature=0.2, timeout=15)52        return _parse(r.choices[0].message.content)53    except Exception as e: logger.warning("Groq: %s",e); return {}54 55def _mistral(prompt, key):56    if not key: return {}57    try:58        r = requests.post("https://api.mistral.ai/v1/chat/completions",59            headers={"Authorization":f"Bearer {key}","Content-Type":"application/json"},60            json={"model":MISTRAL_MODEL,"messages":[{"role":"user","content":prompt}],61                  "temperature":0.2}, timeout=15)62        return _parse(r.json()["choices"][0]["message"]["content"])63    except Exception as e: logger.warning("Mistral: %s",e); return {}64 65def _gemini(prompt, key):66    if not key: return {}67    model = "gemini-2.5-flash"68    for attempt in range(3):69        try:70            r = requests.post(71                f"https://generativelanguage.googleapis.com/v1beta/models/"72                f"{model}:generateContent?key={key}",73                headers={"Content-Type":"application/json"},74                json={"contents":[{"parts":[{"text":prompt}]}],75                      "generationConfig":{"temperature":0.2}}, timeout=60)76            d = r.json()77            if "candidates" not in d:78                err = d.get("error",{})79                msg = err.get("message","") if isinstance(err,dict) else str(err)80                if "quota" in msg.lower() or "rate" in msg.lower():81                    wait = min(40, 10 * (attempt + 1))82                    logger.warning("Gemini rate-limited, waiting %ds…", wait)83                    time.sleep(wait); continue84                logger.warning("Gemini attempt %d: %s", attempt+1, msg)85                return {}86            return _parse(d["candidates"][0]["content"]["parts"][0]["text"])87        except Exception as e:88            logger.warning("Gemini attempt %d: %s", attempt+1, e)89            time.sleep(5)90    return {}91 92# ---------------------------------------------------------------------------93# Topic labelling prompt94# ---------------------------------------------------------------------------95def _label_prompt(keyphrases, rep_docs):96    kp = ", ".join(k[0] if isinstance(k,tuple) else k for k in keyphrases[:5])97    ab = " | ".join(a[:250] for a in rep_docs[:3])98    return f"""You are a research topic classifier.99A SPECTER-2 + HDBSCAN pipeline produced a topic cluster.100 101KEYPHRASES: {kp}102REPRESENTATIVE ABSTRACTS: {ab}103 104Return ONLY valid JSON:105{{106  "label": "<5-8 word topic label>",107  "description": "<one sentence description>",108  "pacis_match": "<closest PAJAIS 2019 category, or NOVEL if none>",109  "confidence": <0.0-1.0>110}}"""111 112# ---------------------------------------------------------------------------113# Defence prompt for disagreements114# ---------------------------------------------------------------------------115def _defence_prompt(keyphrases, rep_docs, votes):116    kp = ", ".join(k[0] if isinstance(k,tuple) else k for k in keyphrases[:5])117    v_str = "\n".join(f"  LLM{i+1}: {v.get('label','?')}" for i,v in enumerate(votes))118    return f"""Resolve this labelling disagreement.119KEYPHRASES: {kp}120Votes:\n{v_str}121Pick the best label or synthesise a better one.122Return ONLY JSON: {{"label":"...","description":"...","pacis_match":"...","confidence":0.0}}"""123 124# ---------------------------------------------------------------------------125# Grounding check126# ---------------------------------------------------------------------------127def _grounding(label, keyphrases):128    if not label or not keyphrases: return {"verdict":"FAIL","score":0}129    lt = set(re.findall(r"\b[a-z]{3,}\b", label.lower()))130    kt = set()131    for k in keyphrases:132        kt.update(re.findall(r"\b[a-z]{3,}\b", (k[0] if isinstance(k,tuple) else k).lower()))133    noise = {"the","and","for","with","using","based","from","that","are","this"}134    lt -= noise; kt -= noise135    m = list(lt & kt)136    return {"verdict":"PASS" if m else "FAIL", "score":len(m)/max(len(lt),1), "matched":m}137 138def _clean(s):139    s = str(s or "").replace("\n"," ").strip()140    return s[:60].rsplit(" ",1)[0] if len(s)>60 else s141 142# ---------------------------------------------------------------------------143# LangGraph node: run topic modelling144# ---------------------------------------------------------------------------145def embed_and_cluster(state: PipelineState) -> dict:146    from tools import run_topic_modeling147    try:148        td = run_topic_modeling(state["filepath"], state.get("n_trials", 50))149        return {"topic_data": td}150    except Exception as e:151        return {"error": str(e)}152 153# ---------------------------------------------------------------------------154# LangGraph node: LLM Council — 4 sheets for topic modelling155# ---------------------------------------------------------------------------156def llm_council(state: PipelineState) -> dict:157    td = state["topic_data"]158    if not td: return {"error": "No topic data"}159    client = Groq(api_key=state["groq_key"], max_retries=0)160    mk, gk = state["mistral_key"], state["gemini_key"]161 162    sheets = {1:[], 2:[], 3:[], 4:[]}  # 1=Groq, 2=Mistral, 3=Gemini, 4=Consolidated163    interps = {}164 165    for cid in sorted(td["keyphrases"].keys()):166        kps = td["keyphrases"][cid]167        rds = td["representative_docs"].get(cid, [])168        sw = td["membership"].get(cid, {"strong":0,"weak":0})169        prompt = _label_prompt(kps, rds)170 171        s1 = _groq(client, prompt); time.sleep(1)172        s2 = _mistral(prompt, mk); time.sleep(1)173        s3 = _gemini(prompt, gk); time.sleep(4)  # respect Gemini free-tier rate limit174        votes = [s1, s2, s3]175 176        # Sheets 1-3177        for si, (sheet_n, resp) in enumerate([(1,s1),(2,s2),(3,s3)]):178            sheets[sheet_n].append({"cluster":cid, **{k:resp.get(k,"—")179                for k in ["label","description","pacis_match","confidence"]}})180 181        # Sheet 4: consolidate182        valid = [v for v in votes if v and "label" in v]183        labels_l = [_clean(v.get("label","")).lower() for v in valid]184        counts = Counter(labels_l)185 186        if any(c>=3 for c in counts.values()):187            agreement = "Triple"188            winner = max(counts, key=counts.get)189            best = next(v for v in valid if _clean(v["label"]).lower()==winner)190        elif any(c>=2 for c in counts.values()):191            agreement = "Two"192            winner = max(counts, key=counts.get)193            best = next(v for v in valid if _clean(v["label"]).lower()==winner)194        else:195            agreement = "Single"196            d = _groq(client, _defence_prompt(kps, rds, votes))197            best = d if d and "label" in d else (valid[0] if valid else {})198 199        label = _clean(best.get("label",""))200        gc = _grounding(label, kps)201        if gc["verdict"]=="FAIL" and valid:202            label = _clean(valid[0].get("label",""))203 204        cp = td.get("cluster_persistence",{}).get(cid, 0.0)205        sheets[4].append({"cluster":cid, "label":label, "agreement":agreement,206            "description":best.get("description",""),207            "pacis_match":best.get("pacis_match",""),208            "strong":sw["strong"], "weak":sw["weak"],209            "persistence":round(cp,4), "grounding":gc["verdict"]})210 211        interps[cid] = {"label":label, "agreement":agreement,212            "strong":sw["strong"], "weak":sw["weak"],213            "persistence":cp, "description":best.get("description",""),214            "pacis_match":best.get("pacis_match",""),215            "keyphrases":[k[0] if isinstance(k,tuple) else k for k in kps[:5]]}216 217        logger.info("Cluster %d → %s [%s]", cid, label, agreement)218 219    # Agreement rate on labels220    total = len(sheets[4]) or 1221    n_triple = sum(1 for r in sheets[4] if r.get("agreement")=="Triple")222    n_two = sum(1 for r in sheets[4] if r.get("agreement")=="Two")223    rates = {224        "triple": round(n_triple / total * 100),225        "two_or_more": round((n_triple + n_two) / total * 100),226        "single": round((total - n_triple - n_two) / total * 100),227    }228 229    # Save outputs — 4 separate sheet files230    sheet_paths = {}231    names = {1:"sheet1_groq",2:"sheet2_mistral",3:"sheet3_gemini",4:"sheet4_consolidated"}232    for sn, name in names.items():233        path = f"{name}.csv"234        pd.DataFrame(sheets[sn]).to_csv(path, index=False)235        sheet_paths[sn] = path236    with open("topics.json","w") as f: json.dump(sheets[4], f, indent=2)237 238    return {"interpretations":interps, "sheets":sheets,239            "agreement_rates":rates, "sheet_paths":sheet_paths,240            "json_path":"topics.json"}241 242# ---------------------------------------------------------------------------243# LangGraph node: build mismatch table244# ---------------------------------------------------------------------------245def build_mismatch(state: PipelineState) -> dict:246    from tools import build_mismatch_table247    td = state["topic_data"]248    interps = state.get("interpretations", {})249    labels_map = {cid: v["label"] for cid, v in interps.items()}250    mt = build_mismatch_table(td["keyphrases"], labels_map)251    return {"mismatch_table": mt}252 253# ---------------------------------------------------------------------------254# Build the LangGraph255# ---------------------------------------------------------------------------256def build_graph() -> StateGraph:257    g = StateGraph(PipelineState)258    g.add_node("embed_and_cluster", embed_and_cluster)259    g.add_node("llm_council", llm_council)260    g.add_node("build_mismatch", build_mismatch)261    g.set_entry_point("embed_and_cluster")262    g.add_edge("embed_and_cluster", "llm_council")263    g.add_edge("llm_council", "build_mismatch")264    g.add_edge("build_mismatch", END)265    return g.compile()266 267# Compiled graph — importable268pipeline_graph = build_graph()269 270def run_pipeline(filepath, groq_key, mistral_key, gemini_key, n_trials=50):271    """Convenience wrapper."""272    result = pipeline_graph.invoke({273        "filepath": filepath,274        "groq_key": groq_key,275        "mistral_key": mistral_key,276        "gemini_key": gemini_key,277        "n_trials": n_trials,278    })279    return result280