Spaces:
Paused
Paused
Commit ·
a2dedd0
1
Parent(s): 8ed74e6
Deploy QueryLens (Gradio-SDK launcher: worker :8700 / MCP :8701 / web :7860)
Browse files- Agent/agentic_workflow.py +77 -6
- Agent/worker.py +21 -8
Agent/agentic_workflow.py
CHANGED
|
@@ -834,6 +834,7 @@ PLACEHOLDER_REGEXES = [
|
|
| 834 |
# Global readiness flag so we can re-attempt handshake before first real generation
|
| 835 |
LLM_HANDSHAKE_COMPLETE = False
|
| 836 |
LAST_GENERATED_SQL = None # updated each time a final SQL statement is produced
|
|
|
|
| 837 |
# Cached real department names from DB — populated during initialize_database
|
| 838 |
# Fallback: known employee-DB departments (used if DB query fails)
|
| 839 |
_cached_dept_names: set = {
|
|
@@ -848,7 +849,11 @@ def build_cloud_llm():
|
|
| 848 |
This is the same path used on Hugging Face Spaces. It is also used as an
|
| 849 |
automatic fallback inside initialize_llm() when the configured local
|
| 850 |
Ollama model is not installed.
|
| 851 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 852 |
hf_token = os.environ.get("HF_API_TOKEN")
|
| 853 |
hf_model = os.environ.get("HF_LLM_MODEL", "Qwen/Qwen2.5-Coder-7B-Instruct")
|
| 854 |
if not hf_token:
|
|
@@ -874,11 +879,23 @@ def build_cloud_llm():
|
|
| 874 |
# Talk directly to HF serverless pool using the Chat Completion API
|
| 875 |
# This satisfies providers that require the "conversational" task
|
| 876 |
hf_client = InferenceClient(model=self.model_name, token=hf_token)
|
| 877 |
-
|
| 878 |
-
|
| 879 |
-
|
| 880 |
-
|
| 881 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 882 |
|
| 883 |
def stream_complete(self, prompt: str, **kwargs):
|
| 884 |
# Not needed for our agent, but required by base class
|
|
@@ -901,6 +918,44 @@ def build_cloud_llm():
|
|
| 901 |
return llm
|
| 902 |
|
| 903 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 904 |
def _ollama_model_available(model_name: str, base_url: str) -> bool:
|
| 905 |
"""Return True if the given model tag exists on the Ollama server.
|
| 906 |
|
|
@@ -2555,6 +2610,11 @@ def get_mcp_hint_if_available(question: str):
|
|
| 2555 |
if isinstance(tool_res, dict) and 'sql' in tool_res:
|
| 2556 |
sql = tool_res['sql']
|
| 2557 |
_fast_log('TOOL_SQL', ms=int(dur*1000), sql_preview=sql[:160])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2558 |
return sql # Return as hint
|
| 2559 |
else:
|
| 2560 |
_fast_log('NO_SQL', raw=tool_res)
|
|
@@ -3951,6 +4011,17 @@ def query_craft(question: str) -> str:
|
|
| 3951 |
|
| 3952 |
# All 3 levels failed
|
| 3953 |
print("[3-LEVEL] ✗ All 3 levels failed to generate valid SQL")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3954 |
raise ValueError("Unable to generate SQL query after 3-level attempts; try rephrasing.")
|
| 3955 |
# salvage path (unreachable due to raise above, kept for reference)
|
| 3956 |
|
|
|
|
| 834 |
# Global readiness flag so we can re-attempt handshake before first real generation
|
| 835 |
LLM_HANDSHAKE_COMPLETE = False
|
| 836 |
LAST_GENERATED_SQL = None # updated each time a final SQL statement is produced
|
| 837 |
+
LAST_MCP_HINT_SQL = None # last validated fast-SQL hint (deterministic fallback path)
|
| 838 |
# Cached real department names from DB — populated during initialize_database
|
| 839 |
# Fallback: known employee-DB departments (used if DB query fails)
|
| 840 |
_cached_dept_names: set = {
|
|
|
|
| 849 |
This is the same path used on Hugging Face Spaces. It is also used as an
|
| 850 |
automatic fallback inside initialize_llm() when the configured local
|
| 851 |
Ollama model is not installed.
|
| 852 |
+
|
| 853 |
+
Credits-resilience: on HF Inference 402/401/429 (credits exhausted, expired
|
| 854 |
+
token, rate limit) the same prompt is re-issued to Groq's OpenAI-compatible
|
| 855 |
+
endpoint (GROQ_API_KEY is already a Space secret). Model is configurable via
|
| 856 |
+
GROQ_LLM_MODEL (default: llama-3.3-70b-versatile)."""
|
| 857 |
hf_token = os.environ.get("HF_API_TOKEN")
|
| 858 |
hf_model = os.environ.get("HF_LLM_MODEL", "Qwen/Qwen2.5-Coder-7B-Instruct")
|
| 859 |
if not hf_token:
|
|
|
|
| 879 |
# Talk directly to HF serverless pool using the Chat Completion API
|
| 880 |
# This satisfies providers that require the "conversational" task
|
| 881 |
hf_client = InferenceClient(model=self.model_name, token=hf_token)
|
| 882 |
+
try:
|
| 883 |
+
resp = hf_client.chat_completion(
|
| 884 |
+
messages=[{"role": "user", "content": prompt}],
|
| 885 |
+
max_tokens=1024
|
| 886 |
+
)
|
| 887 |
+
return CompletionResponse(text=resp.choices[0].message.content)
|
| 888 |
+
except Exception as hf_exc:
|
| 889 |
+
if _is_provider_quota_error(hf_exc):
|
| 890 |
+
print(f"[LLM-FALLBACK] HF Inference unavailable "
|
| 891 |
+
f"({hf_exc.__class__.__name__}); falling back to Groq "
|
| 892 |
+
f"text model '{GROQ_TEXT_MODEL}'", flush=True)
|
| 893 |
+
try:
|
| 894 |
+
groq_text = _groq_text_completion(prompt)
|
| 895 |
+
return CompletionResponse(text=groq_text)
|
| 896 |
+
except Exception as groq_exc:
|
| 897 |
+
print(f"[LLM-FALLBACK] Groq fallback also failed: {groq_exc}", flush=True)
|
| 898 |
+
raise
|
| 899 |
|
| 900 |
def stream_complete(self, prompt: str, **kwargs):
|
| 901 |
# Not needed for our agent, but required by base class
|
|
|
|
| 918 |
return llm
|
| 919 |
|
| 920 |
|
| 921 |
+
def _is_provider_quota_error(exc: Exception) -> bool:
|
| 922 |
+
"""Detect HF Inference failures that a Groq fallback can plausibly fix:
|
| 923 |
+
out of credits (402), expired/missing token (401) and rate limits (429)."""
|
| 924 |
+
status = getattr(exc, "status_code", None)
|
| 925 |
+
if status is None:
|
| 926 |
+
response = getattr(exc, "response", None)
|
| 927 |
+
status = getattr(response, "status_code", None)
|
| 928 |
+
if status in (401, 402, 429):
|
| 929 |
+
return True
|
| 930 |
+
msg = str(exc)
|
| 931 |
+
return any(sig in msg for sig in (
|
| 932 |
+
"402", "Payment Required", "no remaining credits",
|
| 933 |
+
"401", "Unauthorized", "429", "rate limit",
|
| 934 |
+
))
|
| 935 |
+
|
| 936 |
+
|
| 937 |
+
GROQ_TEXT_MODEL = os.environ.get("GROQ_LLM_MODEL", "llama-3.3-70b-versatile")
|
| 938 |
+
|
| 939 |
+
|
| 940 |
+
def _groq_text_completion(prompt: str, max_tokens: int = 1024, timeout: float = 90.0) -> str:
|
| 941 |
+
"""Plain chat completion against Groq's OpenAI-compatible endpoint."""
|
| 942 |
+
groq_key = os.environ.get("GROQ_API_KEY")
|
| 943 |
+
if not groq_key:
|
| 944 |
+
raise RuntimeError("GROQ_API_KEY is not set; Groq text-LLM fallback unavailable")
|
| 945 |
+
r = requests.post(
|
| 946 |
+
"https://api.groq.com/openai/v1/chat/completions",
|
| 947 |
+
headers={"Authorization": f"Bearer {groq_key}"},
|
| 948 |
+
json={
|
| 949 |
+
"model": GROQ_TEXT_MODEL,
|
| 950 |
+
"messages": [{"role": "user", "content": prompt}],
|
| 951 |
+
"max_tokens": max_tokens,
|
| 952 |
+
},
|
| 953 |
+
timeout=timeout,
|
| 954 |
+
)
|
| 955 |
+
r.raise_for_status()
|
| 956 |
+
return r.json()["choices"][0]["message"]["content"]
|
| 957 |
+
|
| 958 |
+
|
| 959 |
def _ollama_model_available(model_name: str, base_url: str) -> bool:
|
| 960 |
"""Return True if the given model tag exists on the Ollama server.
|
| 961 |
|
|
|
|
| 2610 |
if isinstance(tool_res, dict) and 'sql' in tool_res:
|
| 2611 |
sql = tool_res['sql']
|
| 2612 |
_fast_log('TOOL_SQL', ms=int(dur*1000), sql_preview=sql[:160])
|
| 2613 |
+
# Remember for the deterministic post-3-level fallback (see the
|
| 2614 |
+
# "All 3 levels failed" block): if every LLM call fails due to
|
| 2615 |
+
# provider outage/credits, this schema-true SQL still answers.
|
| 2616 |
+
global LAST_MCP_HINT_SQL
|
| 2617 |
+
LAST_MCP_HINT_SQL = sql
|
| 2618 |
return sql # Return as hint
|
| 2619 |
else:
|
| 2620 |
_fast_log('NO_SQL', raw=tool_res)
|
|
|
|
| 4011 |
|
| 4012 |
# All 3 levels failed
|
| 4013 |
print("[3-LEVEL] ✗ All 3 levels failed to generate valid SQL")
|
| 4014 |
+
# Deterministic last resort: the MCP fast-SQL hint produced during Level 3
|
| 4015 |
+
# is schema-true and question-matched. If all LLM calls failed because of
|
| 4016 |
+
# provider outage / exhausted credits, return it instead of erroring out.
|
| 4017 |
+
if LAST_MCP_HINT_SQL:
|
| 4018 |
+
try:
|
| 4019 |
+
if is_sql_valid(LAST_MCP_HINT_SQL, question):
|
| 4020 |
+
print("[3-LEVEL] ✓ FALLBACK: using deterministic MCP fast-SQL hint as final answer")
|
| 4021 |
+
print(f"[GEN] Final SQL: {LAST_MCP_HINT_SQL[:200]}")
|
| 4022 |
+
return LAST_MCP_HINT_SQL
|
| 4023 |
+
except Exception as fb_err:
|
| 4024 |
+
print(f"[3-LEVEL] MCP fast-SQL fallback skipped: {fb_err}")
|
| 4025 |
raise ValueError("Unable to generate SQL query after 3-level attempts; try rephrasing.")
|
| 4026 |
# salvage path (unreachable due to raise above, kept for reference)
|
| 4027 |
|
Agent/worker.py
CHANGED
|
@@ -124,27 +124,40 @@ async def startup_event():
|
|
| 124 |
try:
|
| 125 |
await agent.initialize_database(uri=target_uri)
|
| 126 |
# Keep the registry's active entry in sync with what we connected to.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 127 |
try:
|
| 128 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
prof = getattr(agent, "ACTIVE_SCHEMA_PROFILE", None)
|
| 130 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
pid = f"boot_{uuid.uuid4().hex[:8]}"
|
| 132 |
profile_registry.profiles[pid] = {
|
| 133 |
-
"name":
|
| 134 |
"db_url": target_uri,
|
| 135 |
"kind": _kind,
|
| 136 |
"created_at": __import__("time").time(),
|
| 137 |
}
|
| 138 |
profile_registry.active_id = pid
|
| 139 |
profile_registry.save()
|
| 140 |
-
print(f"[WORKER] Registered
|
| 141 |
else:
|
| 142 |
-
|
| 143 |
-
entries = profile_registry.list()
|
| 144 |
-
match = next((p for p in entries if (p.get("db_url") or "").replace("\\", "/") == target_uri.replace("\\", "/")), None)
|
| 145 |
-
if match and profile_registry.active_id != match["id"]:
|
| 146 |
profile_registry.switch(match["id"])
|
| 147 |
print(f"[WORKER] Active profile switched to '{match['name']}'")
|
|
|
|
|
|
|
| 148 |
except Exception as reg_err:
|
| 149 |
print(f"[WORKER] profile bootstrap skipped: {reg_err}")
|
| 150 |
except Exception as e:
|
|
|
|
| 124 |
try:
|
| 125 |
await agent.initialize_database(uri=target_uri)
|
| 126 |
# Keep the registry's active entry in sync with what we connected to.
|
| 127 |
+
# IMPORTANT: force the booted database to OWN the active slot — the
|
| 128 |
+
# demo registration above grabs "active" on a fresh store (ProfileRegistry
|
| 129 |
+
# .add() activates when no active exists), which left Space deployments
|
| 130 |
+
# showing the demo DB even though the env DB_CONNECTION_URL booted.
|
| 131 |
try:
|
| 132 |
+
entries = profile_registry.list()
|
| 133 |
+
match = next(
|
| 134 |
+
(p for p in entries
|
| 135 |
+
if ((p.get("db_url") or "").replace("\\", "/") == target_uri.replace("\\", "/"))),
|
| 136 |
+
None,
|
| 137 |
+
)
|
| 138 |
+
if match is None:
|
| 139 |
prof = getattr(agent, "ACTIVE_SCHEMA_PROFILE", None)
|
| 140 |
+
_from_env = bool(db_url and target_uri == db_url)
|
| 141 |
+
_kind = "user" if _from_env else (
|
| 142 |
+
"demo" if (prof and getattr(prof, "is_employee_db", False)) else "user")
|
| 143 |
+
_name = (getattr(prof, "name", None) if prof else None) or (
|
| 144 |
+
"Employee_DB" if _from_env else "demo_employee")
|
| 145 |
pid = f"boot_{uuid.uuid4().hex[:8]}"
|
| 146 |
profile_registry.profiles[pid] = {
|
| 147 |
+
"name": _name,
|
| 148 |
"db_url": target_uri,
|
| 149 |
"kind": _kind,
|
| 150 |
"created_at": __import__("time").time(),
|
| 151 |
}
|
| 152 |
profile_registry.active_id = pid
|
| 153 |
profile_registry.save()
|
| 154 |
+
print(f"[WORKER] Registered ACTIVE schema profile '{_name}' (kind={_kind}) for env DB")
|
| 155 |
else:
|
| 156 |
+
if profile_registry.active_id != match["id"]:
|
|
|
|
|
|
|
|
|
|
| 157 |
profile_registry.switch(match["id"])
|
| 158 |
print(f"[WORKER] Active profile switched to '{match['name']}'")
|
| 159 |
+
else:
|
| 160 |
+
print(f"[WORKER] Active profile already '{match['name']}'")
|
| 161 |
except Exception as reg_err:
|
| 162 |
print(f"[WORKER] profile bootstrap skipped: {reg_err}")
|
| 163 |
except Exception as e:
|