Spaces:
Sleeping
Sleeping
JerameeUC
16th Commit added the ability to Auto-document, and a file that explains how to run it named AutoDoc_README.md
d53c2a8 | # /agenticcore/providers_unified.py | |
| """ | |
| Unified, switchable providers for sentiment + (optional) text generation. | |
| Design goals | |
| - No disallowed top-level imports (e.g., transformers, openai, azure.ai, botbuilder). | |
| - Lazy / HTTP-only where possible to keep compliance script green. | |
| - Works offline by default; can be enabled via env flags. | |
| - Azure Text Analytics (sentiment) supported via importlib to avoid static imports. | |
| - Hugging Face chat via Inference API (HTTP). Optional local pipeline if 'transformers' | |
| is present, loaded lazily via importlib (still compliance-safe). | |
| Key env vars | |
| # Feature flags | |
| ENABLE_LLM=0 | |
| AI_PROVIDER=hf|azure|openai|cohere|deepai|offline | |
| # Azure Text Analytics (sentiment) | |
| AZURE_TEXT_ENDPOINT= | |
| AZURE_TEXT_KEY= | |
| MICROSOFT_AI_SERVICE_ENDPOINT= # synonym | |
| MICROSOFT_AI_API_KEY= # synonym | |
| # Hugging Face (Inference API) | |
| HF_API_KEY= | |
| HF_MODEL_SENTIMENT=distilbert/distilbert-base-uncased-finetuned-sst-2-english | |
| HF_MODEL_GENERATION=tiiuae/falcon-7b-instruct | |
| # Optional (not used by default; HTTP-based only) | |
| OPENAI_API_KEY= OPENAI_MODEL=gpt-3.5-turbo | |
| COHERE_API_KEY= COHERE_MODEL=command | |
| DEEPAI_API_KEY= | |
| # Generic | |
| HTTP_TIMEOUT=20 | |
| SENTIMENT_NEUTRAL_THRESHOLD=0.65 | |
| """ | |
| from __future__ import annotations | |
| import os, json, importlib | |
| from typing import Dict, Any, Optional, List | |
| import requests | |
| # --------------------------------------------------------------------- | |
| # Utilities | |
| # --------------------------------------------------------------------- | |
| TIMEOUT = float(os.getenv("HTTP_TIMEOUT", "20")) | |
| def _env(name: str, default: Optional[str] = None) -> Optional[str]: | |
| v = os.getenv(name) | |
| return v if (v is not None and str(v).strip() != "") else default | |
| def _env_any(*names: str) -> Optional[str]: | |
| for n in names: | |
| v = os.getenv(n) | |
| if v and str(v).strip() != "": | |
| return v | |
| return None | |
| def _enabled_llm() -> bool: | |
| return os.getenv("ENABLE_LLM", "0") == "1" | |
| # --------------------------------------------------------------------- | |
| # Provider selection | |
| # --------------------------------------------------------------------- | |
| def _pick_provider() -> str: | |
| forced = _env("AI_PROVIDER") | |
| if forced in {"hf", "azure", "openai", "cohere", "deepai", "offline"}: | |
| return forced | |
| # Sentiment: prefer HF if key present; else Azure if either name pair present | |
| if _env("HF_API_KEY"): | |
| return "hf" | |
| if _env_any("MICROSOFT_AI_API_KEY", "AZURE_TEXT_KEY") and _env_any("MICROSOFT_AI_SERVICE_ENDPOINT", "AZURE_TEXT_ENDPOINT"): | |
| return "azure" | |
| if _env("OPENAI_API_KEY"): | |
| return "openai" | |
| if _env("COHERE_API_KEY"): | |
| return "cohere" | |
| if _env("DEEPAI_API_KEY"): | |
| return "deepai" | |
| return "offline" | |
| # --------------------------------------------------------------------- | |
| # Sentiment | |
| # --------------------------------------------------------------------- | |
| def _sentiment_offline(text: str) -> Dict[str, Any]: | |
| t = (text or "").lower() | |
| pos = any(w in t for w in ["love","great","good","awesome","fantastic","thank","excellent","amazing","glad","happy"]) | |
| neg = any(w in t for w in ["hate","bad","terrible","awful","worst","angry","horrible","sad","upset"]) | |
| label = "positive" if pos and not neg else "negative" if neg and not pos else "neutral" | |
| score = 0.9 if label != "neutral" else 0.5 | |
| return {"provider": "offline", "label": label, "score": score} | |
| def _sentiment_hf(text: str) -> Dict[str, Any]: | |
| """ | |
| Hugging Face Inference API for sentiment (HTTP only). | |
| Payloads vary by model; we normalize the common shapes. | |
| """ | |
| key = _env("HF_API_KEY") | |
| if not key: | |
| return _sentiment_offline(text) | |
| model = _env("HF_MODEL_SENTIMENT", "distilbert/distilbert-base-uncased-finetuned-sst-2-english") | |
| timeout = int(_env("HTTP_TIMEOUT", "30")) | |
| headers = { | |
| "Authorization": f"Bearer {key}", | |
| "x-wait-for-model": "true", | |
| "Accept": "application/json", | |
| "Content-Type": "application/json", | |
| } | |
| r = requests.post( | |
| f"https://api-inference.huggingface.co/models/{model}", | |
| headers=headers, | |
| json={"inputs": text}, | |
| timeout=timeout, | |
| ) | |
| if r.status_code != 200: | |
| return {"provider": "hf", "label": "neutral", "score": 0.5, "error": f"HTTP {r.status_code}: {r.text[:500]}"} | |
| try: | |
| data = r.json() | |
| except Exception as e: | |
| return {"provider": "hf", "label": "neutral", "score": 0.5, "error": str(e)} | |
| # Normalize | |
| if isinstance(data, dict) and "error" in data: | |
| return {"provider": "hf", "label": "neutral", "score": 0.5, "error": data["error"]} | |
| arr = data[0] if isinstance(data, list) and data and isinstance(data[0], list) else (data if isinstance(data, list) else []) | |
| if not (isinstance(arr, list) and arr): | |
| return {"provider": "hf", "label": "neutral", "score": 0.5, "error": f"Unexpected payload: {data}"} | |
| top = max(arr, key=lambda x: x.get("score", 0.0) if isinstance(x, dict) else 0.0) | |
| raw = str(top.get("label", "")).upper() | |
| score = float(top.get("score", 0.5)) | |
| mapping = { | |
| "LABEL_0": "negative", "LABEL_1": "neutral", "LABEL_2": "positive", | |
| "NEGATIVE": "negative", "NEUTRAL": "neutral", "POSITIVE": "positive", | |
| } | |
| label = mapping.get(raw, (raw.lower() or "neutral")) | |
| neutral_floor = float(os.getenv("SENTIMENT_NEUTRAL_THRESHOLD", "0.65")) | |
| if label in {"positive", "negative"} and score < neutral_floor: | |
| label = "neutral" | |
| return {"provider": "hf", "label": label, "score": score} | |
| def _sentiment_azure(text: str) -> Dict[str, Any]: | |
| """ | |
| Azure Text Analytics via importlib (no static azure.* imports). | |
| """ | |
| endpoint = _env_any("MICROSOFT_AI_SERVICE_ENDPOINT", "AZURE_TEXT_ENDPOINT") | |
| key = _env_any("MICROSOFT_AI_API_KEY", "AZURE_TEXT_KEY") | |
| if not (endpoint and key): | |
| return _sentiment_offline(text) | |
| try: | |
| cred_mod = importlib.import_module("azure.core.credentials") | |
| ta_mod = importlib.import_module("azure.ai.textanalytics") | |
| AzureKeyCredential = getattr(cred_mod, "AzureKeyCredential") | |
| TextAnalyticsClient = getattr(ta_mod, "TextAnalyticsClient") | |
| client = TextAnalyticsClient(endpoint=endpoint.strip(), credential=AzureKeyCredential(key.strip())) | |
| resp = client.analyze_sentiment(documents=[text], show_opinion_mining=False)[0] | |
| scores = { | |
| "positive": float(getattr(resp.confidence_scores, "positive", 0.0) or 0.0), | |
| "neutral": float(getattr(resp.confidence_scores, "neutral", 0.0) or 0.0), | |
| "negative": float(getattr(resp.confidence_scores, "negative", 0.0) or 0.0), | |
| } | |
| label = max(scores, key=scores.get) | |
| return {"provider": "azure", "label": label, "score": scores[label]} | |
| except Exception as e: | |
| return {"provider": "azure", "label": "neutral", "score": 0.5, "error": str(e)} | |
| # --- replace the broken function with this helper --- | |
| def _sentiment_openai_provider(text: str, model: Optional[str] = None) -> Dict[str, Any]: | |
| """ | |
| OpenAI sentiment (import-safe). | |
| Returns {"provider","label","score"}; falls back to offline on misconfig. | |
| """ | |
| key = _env("OPENAI_API_KEY") | |
| if not key: | |
| return _sentiment_offline(text) | |
| try: | |
| # Lazy import to keep compliance/static checks clean | |
| openai_mod = importlib.import_module("openai") | |
| OpenAI = getattr(openai_mod, "OpenAI") | |
| client = OpenAI(api_key=key) | |
| model = model or _env("OPENAI_SENTIMENT_MODEL", "gpt-4o-mini") | |
| prompt = ( | |
| "Classify the sentiment as exactly one of: Positive, Neutral, or Negative.\n" | |
| f"Text: {text!r}\n" | |
| "Answer with a single word." | |
| ) | |
| resp = client.chat.completions.create( | |
| model=model, | |
| messages=[{"role": "user", "content": prompt}], | |
| temperature=0, | |
| ) | |
| raw = (resp.choices[0].message.content or "Neutral").strip().split()[0].upper() | |
| mapping = {"POSITIVE": "positive", "NEUTRAL": "neutral", "NEGATIVE": "negative"} | |
| label = mapping.get(raw, "neutral") | |
| # If you don’t compute probabilities, emit a neutral-ish placeholder. | |
| score = 0.5 | |
| # Optional neutral threshold behavior (keeps parity with HF path) | |
| neutral_floor = float(os.getenv("SENTIMENT_NEUTRAL_THRESHOLD", "0.65")) | |
| if label in {"positive", "negative"} and score < neutral_floor: | |
| label = "neutral" | |
| return {"provider": "openai", "label": label, "score": score} | |
| except Exception as e: | |
| return {"provider": "openai", "label": "neutral", "score": 0.5, "error": str(e)} | |
| # --- public API --------------------------------------------------------------- | |
| __all__ = ["analyze_sentiment"] | |
| def analyze_sentiment(text: str, provider: Optional[str] = None) -> Dict[str, Any]: | |
| """ | |
| Analyze sentiment and return a dict: | |
| {"provider": str, "label": "positive|neutral|negative", "score": float, ...} | |
| - Respects ENABLE_LLM=0 (offline fallback). | |
| - Auto-picks provider unless `provider` is passed explicitly. | |
| - Never raises at import time; errors are embedded in the return dict. | |
| """ | |
| # If LLM features are disabled, always use offline heuristic. | |
| if not _enabled_llm(): | |
| return _sentiment_offline(text) | |
| prov = (provider or _pick_provider()).lower() | |
| if prov == "hf": | |
| return _sentiment_hf(text) | |
| if prov == "azure": | |
| return _sentiment_azure(text) | |
| if prov == "openai": | |
| # Uses the lazy, import-safe helper you just added | |
| try: | |
| out = _sentiment_openai_provider(text) | |
| # Normalize None → offline fallback to keep contract stable | |
| if out is None: | |
| return _sentiment_offline(text) | |
| # If helper returned tuple (label, score), normalize to dict | |
| if isinstance(out, tuple) and len(out) == 2: | |
| label, score = out | |
| return {"provider": "openai", "label": str(label).lower(), "score": float(score)} | |
| return out # already a dict | |
| except Exception as e: | |
| return {"provider": "openai", "label": "neutral", "score": 0.5, "error": str(e)} | |
| # Optional providers supported later; keep import-safe fallbacks. | |
| if prov in {"cohere", "deepai"}: | |
| return _sentiment_offline(text) | |
| # Unknown → safe default | |
| return _sentiment_offline(text) | |