starfriend commited on
Commit
30f6ce2
·
verified ·
1 Parent(s): 1c647d5

Upload 2 files

Browse files
backend/research_fallback.py CHANGED
@@ -1,7 +1,8 @@
1
  """Bounded DeepSeek fallback for publication-grounded research answers.
2
 
3
- Only called after eligible Llama infrastructure failures (or while a short
4
- circuit breaker is active); never changes retrieval or media analysis.
 
5
  """
6
  from __future__ import annotations
7
 
@@ -11,10 +12,40 @@ import logging
11
  import threading
12
  import time
13
 
 
 
14
  _LOCK = threading.Lock()
15
  _UNAVAILABLE_UNTIL = 0.0
16
 
17
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  def is_infrastructure_error(exc: Exception) -> bool:
19
  text = f"{type(exc).__name__}: {exc}".lower()
20
  signals = (
@@ -25,12 +56,8 @@ def is_infrastructure_error(exc: Exception) -> bool:
25
  "503 service", "504 gateway", "gradio", "network error",
26
  )
27
  return any(signal in text for signal in signals)
28
-
29
-
30
-
31
  def is_gradio_transport_json_error(exc: Exception) -> bool:
32
  """Classify only malformed JSON returned while contacting the Llama Space.
33
-
34
  Never treat JSON parsing failures in publication retrieval or our own
35
  application data as an infrastructure outage.
36
  """
@@ -43,7 +70,6 @@ def is_gradio_transport_json_error(exc: Exception) -> bool:
43
  return True
44
  tb = tb.tb_next
45
  return False
46
-
47
  def llama_circuit_open() -> bool:
48
  with _LOCK:
49
  return time.monotonic() < _UNAVAILABLE_UNTIL
@@ -61,11 +87,9 @@ def clear_llama_circuit() -> None:
61
  with _LOCK:
62
  _UNAVAILABLE_UNTIL = 0.0
63
 
64
-
65
  def deepseek_research_answer(context: str, question: str, application: str) -> tuple[str, str]:
66
  """Answer from supplied retrieved evidence, without independently browsing papers."""
67
  from openai import OpenAI
68
-
69
  key = os.getenv("DEEPSEEK_API_KEY", "").strip()
70
  if not key:
71
  raise RuntimeError("DeepSeek research fallback is not configured (missing DEEPSEEK_API_KEY)")
 
1
  """Bounded DeepSeek fallback for publication-grounded research answers.
2
 
3
+ Called when the Llama Space is explicitly paused, after eligible Llama
4
+ infrastructure failures, or while a short circuit breaker is active.
5
+ Never changes retrieval or media analysis.
6
  """
7
  from __future__ import annotations
8
 
 
12
  import threading
13
  import time
14
 
15
+ import httpx
16
+
17
  _LOCK = threading.Lock()
18
  _UNAVAILABLE_UNTIL = 0.0
19
 
20
 
21
+ def llama_space_paused() -> bool:
22
+ """Check HF runtime without waking the GPU Space.
23
+
24
+ A confirmed PAUSED state skips Gradio entirely. If the Hugging Face
25
+ status API cannot be reached, retain the existing Llama request and
26
+ error-triggered fallback behavior rather than misclassifying it.
27
+ """
28
+ space_id = os.getenv("WATERSCOPE_SPACE_ID", "EnvironmentalAI/WaterScopeAI")
29
+ token = os.getenv("HF_TOKEN") or os.getenv("HUGGINGFACE_HUB_TOKEN")
30
+ headers = {"Authorization": f"Bearer {token}"} if token else {}
31
+
32
+ try:
33
+ response = httpx.get(
34
+ f"https://huggingface.co/api/spaces/{space_id}/runtime",
35
+ headers=headers,
36
+ timeout=3.0,
37
+ )
38
+ response.raise_for_status()
39
+ payload = response.json()
40
+ stage = str(payload.get("stage", "")).upper() if isinstance(payload, dict) else ""
41
+ return stage == "PAUSED"
42
+ except (httpx.HTTPError, ValueError, TypeError) as exc:
43
+ logging.getLogger(__name__).debug(
44
+ "Could not check Llama Space runtime status; proceeding with normal request: %s",
45
+ type(exc).__name__,
46
+ )
47
+ return False
48
+
49
  def is_infrastructure_error(exc: Exception) -> bool:
50
  text = f"{type(exc).__name__}: {exc}".lower()
51
  signals = (
 
56
  "503 service", "504 gateway", "gradio", "network error",
57
  )
58
  return any(signal in text for signal in signals)
 
 
 
59
  def is_gradio_transport_json_error(exc: Exception) -> bool:
60
  """Classify only malformed JSON returned while contacting the Llama Space.
 
61
  Never treat JSON parsing failures in publication retrieval or our own
62
  application data as an infrastructure outage.
63
  """
 
70
  return True
71
  tb = tb.tb_next
72
  return False
 
73
  def llama_circuit_open() -> bool:
74
  with _LOCK:
75
  return time.monotonic() < _UNAVAILABLE_UNTIL
 
87
  with _LOCK:
88
  _UNAVAILABLE_UNTIL = 0.0
89
 
 
90
  def deepseek_research_answer(context: str, question: str, application: str) -> tuple[str, str]:
91
  """Answer from supplied retrieved evidence, without independently browsing papers."""
92
  from openai import OpenAI
 
93
  key = os.getenv("DEEPSEEK_API_KEY", "").strip()
94
  if not key:
95
  raise RuntimeError("DeepSeek research fallback is not configured (missing DEEPSEEK_API_KEY)")
backend/waterscope_agent.py CHANGED
@@ -1,114 +1,113 @@
1
- """Client for the WaterScopeAI Hugging Face Space /agent_run endpoint."""
2
-
3
- from __future__ import annotations
4
-
5
- import concurrent.futures
6
- import os
7
- import re
8
- from functools import lru_cache
9
- from typing import Any
10
-
11
- from gradio_client import Client
12
-
13
- SPACE_ID = os.getenv("WATERSCOPE_SPACE_ID", "EnvironmentalAI/WaterScopeAI")
14
- TIMEOUT_SEC = int(os.getenv("WATERSCOPE_TIMEOUT_SEC", "600"))
15
-
16
- _SPACE_EXECUTOR = concurrent.futures.ThreadPoolExecutor(
17
- max_workers=2,
18
- thread_name_prefix="waterscope-space",
19
- )
20
-
21
- MODELS = ("base", "dpo", "da_dpo", "tulucore")
22
- APPLICATIONS = (
23
- "general_qa",
24
- "document_qa",
25
- "paper_synthesis",
26
- "method_recommendation",
27
- "research_gaps",
28
- "claim_check",
29
- )
30
-
31
- _SYNTHESIS = re.compile(r"\b(summarize|summary|overview|synthesize|main finding)\b", re.I)
32
- _METHODS = re.compile(r"\b(method|approach|model|algorithm|recommend)\b", re.I)
33
- _GAPS = re.compile(r"\b(gap|limitation|future work|open question|unclear)\b", re.I)
34
- _CLAIM = re.compile(r"\b(does the paper|is it true|claim|support|evidence that)\b", re.I)
35
-
36
-
37
- def infer_application(question: str, *, explicit: str | None = None) -> str:
38
- if explicit and explicit != "auto":
39
- if explicit not in APPLICATIONS:
40
- raise ValueError(f"Unknown application {explicit!r}")
41
- return explicit
42
- text = question.lower()
43
- if _SYNTHESIS.search(text):
44
- return "paper_synthesis"
45
- if _GAPS.search(text):
46
- return "research_gaps"
47
- if _CLAIM.search(text):
48
- return "claim_check"
49
- if _METHODS.search(text):
50
- return "method_recommendation"
51
- return "document_qa"
52
-
53
-
54
- @lru_cache(maxsize=1)
55
- def _client() -> Client:
56
- token = os.getenv("HF_TOKEN") or os.getenv("HUGGINGFACE_HUB_TOKEN")
57
- kwargs: dict[str, Any] = {"httpx_kwargs": {"timeout": TIMEOUT_SEC}}
58
- if token:
59
- kwargs["hf_token"] = token
60
- return Client(SPACE_ID, **kwargs)
61
-
62
-
63
- def run_agent(
64
- *,
65
- context: str,
66
- question: str,
67
- model: str | None = None,
68
- application: str = "document_qa",
69
- max_new_tokens: int | None = None,
70
- ) -> tuple[str, str]:
71
- """Call /agent_run. Returns (answer, diagnostics)."""
72
- selected_model = model or os.getenv("WATERSCOPE_MODEL", "dpo")
73
- if selected_model not in MODELS:
74
- raise ValueError(f"Unknown model {selected_model!r}. Choose from: {MODELS}")
75
-
76
- app_type = infer_application(question, explicit=application if application != "auto" else None)
77
- tokens = max_new_tokens or int(os.getenv("WATERSCOPE_MAX_TOKENS", "1024"))
78
-
79
- def _predict() -> tuple[str, str]:
80
- response = _client().predict(
81
- selected_model,
82
- app_type,
83
- context,
84
- question,
85
- tokens,
86
- api_name="/agent_run",
87
- )
88
- answer = response[0] if response else ""
89
- diagnostics = response[1] if len(response) > 1 else ""
90
- return answer, diagnostics
91
-
92
- # Run in a worker thread so Gradio asyncio does not clash with MCP's loop.
93
- answer, diagnostics = _SPACE_EXECUTOR.submit(_predict).result(timeout=TIMEOUT_SEC)
94
- return answer, diagnostics
95
-
96
-
97
- def agent_answer(
98
- context: str,
99
- question: str,
100
- *,
101
- application: str = "auto",
102
- model: str | None = None,
103
- max_new_tokens: int | None = None,
104
- ) -> tuple[str, str, str]:
105
- """Returns (answer, application_used, diagnostics)."""
106
- app_type = infer_application(question, explicit=application if application != "auto" else None)
107
- answer, diagnostics = run_agent(
108
- context=context,
109
- question=question,
110
- model=model,
111
- application=app_type,
112
- max_new_tokens=max_new_tokens,
113
- )
114
- return answer, app_type, diagnostics
 
1
+ """Client for the WaterScopeAI Hugging Face Space /agent_run endpoint."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import concurrent.futures
6
+ import os
7
+ import re
8
+ from functools import lru_cache
9
+ from typing import Any
10
+
11
+ from gradio_client import Client
12
+
13
+ SPACE_ID = os.getenv("WATERSCOPE_SPACE_ID", "EnvironmentalAI/WaterScopeAI")
14
+ TIMEOUT_SEC = int(os.getenv("WATERSCOPE_TIMEOUT_SEC", "300"))
15
+
16
+ _SPACE_EXECUTOR = concurrent.futures.ThreadPoolExecutor(
17
+ max_workers=2,
18
+ thread_name_prefix="waterscope-space",
19
+ )
20
+
21
+ MODELS = ("base", "dpo", "da_dpo", "tulucore")
22
+ APPLICATIONS = (
23
+ "general_qa",
24
+ "document_qa",
25
+ "paper_synthesis",
26
+ "method_recommendation",
27
+ "research_gaps",
28
+ "claim_check",
29
+ )
30
+
31
+ _SYNTHESIS = re.compile(r"\b(summarize|summary|overview|synthesize|main finding)\b", re.I)
32
+ _METHODS = re.compile(r"\b(method|approach|model|algorithm|recommend)\b", re.I)
33
+ _GAPS = re.compile(r"\b(gap|limitation|future work|open question|unclear)\b", re.I)
34
+ _CLAIM = re.compile(r"\b(does the paper|is it true|claim|support|evidence that)\b", re.I)
35
+
36
+
37
+ def infer_application(question: str, *, explicit: str | None = None) -> str:
38
+ if explicit and explicit != "auto":
39
+ if explicit not in APPLICATIONS:
40
+ raise ValueError(f"Unknown application {explicit!r}")
41
+ return explicit
42
+ text = question.lower()
43
+ if _SYNTHESIS.search(text):
44
+ return "paper_synthesis"
45
+ if _GAPS.search(text):
46
+ return "research_gaps"
47
+ if _CLAIM.search(text):
48
+ return "claim_check"
49
+ if _METHODS.search(text):
50
+ return "method_recommendation"
51
+ return "document_qa"
52
+
53
+
54
+ @lru_cache(maxsize=1)
55
+ def _client() -> Client:
56
+ token = os.getenv("HF_TOKEN") or os.getenv("HUGGINGFACE_HUB_TOKEN")
57
+ kwargs: dict[str, Any] = {"httpx_kwargs": {"timeout": TIMEOUT_SEC}}
58
+ if token:
59
+ kwargs["hf_token"] = token
60
+ return Client(SPACE_ID, **kwargs)
61
+
62
+
63
+ def run_agent(
64
+ *,
65
+ context: str,
66
+ question: str,
67
+ model: str | None = None,
68
+ application: str = "document_qa",
69
+ max_new_tokens: int | None = None,
70
+ ) -> tuple[str, str]:
71
+ """Call /agent_run. Returns (answer, diagnostics)."""
72
+ selected_model = model or os.getenv("WATERSCOPE_MODEL", "dpo")
73
+ if selected_model not in MODELS:
74
+ raise ValueError(f"Unknown model {selected_model!r}. Choose from: {MODELS}")
75
+ app_type = infer_application(question, explicit=application if application != "auto" else None)
76
+ tokens = max_new_tokens or int(os.getenv("WATERSCOPE_MAX_TOKENS", "1024"))
77
+
78
+ def _predict() -> tuple[str, str]:
79
+ response = _client().predict(
80
+ selected_model,
81
+ app_type,
82
+ context,
83
+ question,
84
+ tokens,
85
+ api_name="/agent_run",
86
+ )
87
+ answer = response[0] if response else ""
88
+ diagnostics = response[1] if len(response) > 1 else ""
89
+ return answer, diagnostics
90
+
91
+ # Run in a worker thread so Gradio asyncio does not clash with MCP's loop.
92
+ answer, diagnostics = _SPACE_EXECUTOR.submit(_predict).result(timeout=TIMEOUT_SEC)
93
+ return answer, diagnostics
94
+
95
+
96
+ def agent_answer(
97
+ context: str,
98
+ question: str,
99
+ *,
100
+ application: str = "auto",
101
+ model: str | None = None,
102
+ max_new_tokens: int | None = None,
103
+ ) -> tuple[str, str, str]:
104
+ """Returns (answer, application_used, diagnostics)."""
105
+ app_type = infer_application(question, explicit=application if application != "auto" else None)
106
+ answer, diagnostics = run_agent(
107
+ context=context,
108
+ question=question,
109
+ model=model,
110
+ application=app_type,
111
+ max_new_tokens=max_new_tokens,
112
+ )
113
+ return answer, app_type, diagnostics