starfriend commited on
Commit
4273dfe
·
verified ·
1 Parent(s): a4266f7

Upload 6 files

Browse files
llm_services/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Optional external LLM services for WaterScopeAI-Search."""
llm_services/agent_prompts.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Exact WaterScopeAI agent prompt text copied from the public model Space app.py.
2
+ Source: EnvironmentalAI/WaterScopeAI, commit b248182, app.py agent_run().
3
+ Do not edit text without incrementing the prompt version.
4
+ """
5
+ import hashlib
6
+ import json
7
+
8
+ PROMPT_VERSION = "waterscope-agent-b248182"
9
+ AGENT_APPLICATIONS = {
10
+ "general_qa": {"label": "General environmental QA", "instruction": "Answer the technical question directly. Distinguish facts, assumptions, and recommendations."},
11
+ "document_qa": {"label": "Document-grounded QA", "instruction": "Use the supplied context as the only source for document-specific claims. State clearly when the context does not support an answer."},
12
+ "paper_synthesis": {"label": "Multi-paper synthesis", "instruction": "Compare the supplied papers with explicit source attribution. Preserve key methods and quantitative findings and do not mix evidence among papers."},
13
+ "method_recommendation": {"label": "Method recommendation", "instruction": "Recommend a method using explicit evidence, implementation constraints, limitations, and uncertainty. Do not claim superiority unless the supplied evidence directly supports it."},
14
+ "research_gaps": {"label": "Research-gap identification", "instruction": "Identify evidence-grounded research gaps. Link every gap to a specific limitation or unresolved issue in the supplied context."},
15
+ "claim_check": {"label": "Unsupported-claim check", "instruction": "Evaluate whether the requested claim is supported. Refuse to invent a paper, method, value, causal relationship, or percentage."},
16
+ }
17
+
18
+ def build_agent_messages(application: str, context: str, question: str) -> list[dict[str, str]]:
19
+ """Mirrors WaterScopeAI app.py agent_run() system/user message construction."""
20
+ if application not in AGENT_APPLICATIONS:
21
+ raise ValueError(f"Unknown application: {application}")
22
+ if not isinstance(question, str) or not question.strip():
23
+ raise ValueError("question must be nonempty")
24
+ application_config = AGENT_APPLICATIONS[application]
25
+ system_prompt = f"""
26
+ You are WaterScope-AI, an expert scientific agent for water, wastewater,
27
+ environmental engineering, and sustainability.
28
+
29
+ Application: {application_config["label"]}
30
+ Task behavior: {application_config["instruction"]}
31
+
32
+ Core requirements:
33
+ - Answer the user's actual task directly.
34
+ - Preserve important quantitative values and units.
35
+ - Preserve proper nouns, geographic names, paper titles, symbols, and
36
+ non-ASCII scientific notation exactly as they appear in the supplied context.
37
+ - Do not translate or anglicize geographic names unless the user asks.
38
+ - Attribute document-specific evidence clearly.
39
+ - Separate evidence from inference and recommendation.
40
+ - Never invent papers, citations, numerical values, standards, or findings.
41
+ - When evidence is insufficient, say exactly what cannot be concluded.
42
+ - Avoid repeated sentences or sections and stop when complete.
43
+ """.strip()
44
+ context = (context or "").strip()
45
+ user_content = question.strip()
46
+ if context:
47
+ user_content = (
48
+ "SUPPLIED CONTEXT\n"
49
+ "================\n"
50
+ f"{context}\n\n"
51
+ "USER TASK\n"
52
+ "=========\n"
53
+ f"{question.strip()}"
54
+ )
55
+ return [{"role": "system", "content": system_prompt}, {"role": "user", "content": user_content}]
56
+
57
+ def prompt_sha256(messages: list[dict]) -> str:
58
+ return hashlib.sha256(json.dumps(messages, ensure_ascii=False, sort_keys=True).encode('utf-8')).hexdigest()
llm_services/llm_client.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Unified optional external-model call; no changes to HF inference routing."""
2
+ import os
3
+ import time
4
+ from .agent_prompts import build_agent_messages, prompt_sha256, PROMPT_VERSION
5
+ from .llm_usage import usage_summary
6
+
7
+ def generate(messages, *, provider='azure', model=None, max_completion_tokens=1200):
8
+ if os.getenv("ENABLE_EXTERNAL_LLM", "false").lower() not in {"1", "true", "yes"}:
9
+ raise RuntimeError("External LLM calls are disabled (ENABLE_EXTERNAL_LLM=false)")
10
+ if provider != "azure":
11
+ raise ValueError(f"Unsupported provider: {provider}. Azure is implemented in Stage 1.")
12
+ from .providers.azure import complete
13
+ started = time.monotonic()
14
+ result = complete(messages, model=model, max_completion_tokens=max_completion_tokens)
15
+ result['elapsed_s'] = round(time.monotonic() - started, 3)
16
+ result['metering'] = usage_summary(result)
17
+ return result
18
+
19
+ def run_application(*, application, context, question, provider='azure', model=None, max_completion_tokens=1200):
20
+ messages = build_agent_messages(application, context, question)
21
+ result = generate(messages, provider=provider, model=model, max_completion_tokens=max_completion_tokens)
22
+ result['application'] = application
23
+ result['prompt_version'] = PROMPT_VERSION
24
+ result['prompt_sha256'] = prompt_sha256(messages)
25
+ return result
llm_services/llm_usage.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Safe usage summaries; no prompts or credential values are stored.
2
+ Provide provider-specific prices through environment variables if desired;
3
+ otherwise estimated_cost_usd is None (not an asserted price).
4
+ """
5
+ import os
6
+
7
+ def estimate_cost_usd(provider: str, usage: dict):
8
+ prefix = provider.upper()
9
+ in_price = os.getenv(f"{prefix}_INPUT_USD_PER_M")
10
+ out_price = os.getenv(f"{prefix}_OUTPUT_USD_PER_M")
11
+ if not in_price or not out_price:
12
+ return None
13
+ n_in = usage.get("input_tokens")
14
+ n_out = usage.get("output_tokens")
15
+ if n_in is None or n_out is None:
16
+ return None
17
+ try:
18
+ cost = (n_in * float(in_price) + n_out * float(out_price)) / 1_000_000
19
+ return round(cost, 8)
20
+ except (ValueError, TypeError):
21
+ return None
22
+
23
+ def usage_summary(result: dict):
24
+ return {
25
+ "provider": result["provider"],
26
+ "model": result["model_requested"],
27
+ "elapsed_s": result.get("elapsed_s"),
28
+ "usage": result.get("usage", {}),
29
+ "estimated_cost_usd": estimate_cost_usd(result["provider"], result.get("usage", {})),
30
+ }
llm_services/providers/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Provider adapters."""
llm_services/providers/azure.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Azure Chat Completions adapter. Secrets never appear in responses/logs."""
2
+ import os
3
+ from openai import AzureOpenAI
4
+
5
+ def create_client():
6
+ endpoint = os.environ["AZURE_OPENAI_ENDPOINT"]
7
+ key = os.environ["AZURE_OPENAI_API_KEY"]
8
+ return AzureOpenAI(
9
+ azure_endpoint=endpoint,
10
+ api_key=key,
11
+ api_version=os.getenv("AZURE_OPENAI_API_VERSION", "2024-12-01-preview"),
12
+ timeout=float(os.getenv("EXTERNAL_LLM_TIMEOUT_S", "120")),
13
+ max_retries=1,
14
+ )
15
+
16
+ def complete(messages, *, model=None, max_completion_tokens=1200):
17
+ deployment = model or os.getenv("AZURE_LLM_DEPLOYMENT", "gpt-5.4-mini")
18
+ response = create_client().chat.completions.create(
19
+ model=deployment,
20
+ messages=messages,
21
+ max_completion_tokens=max_completion_tokens,
22
+ )
23
+ choice = response.choices[0]
24
+ usage = response.usage
25
+ details = getattr(usage, "prompt_tokens_details", None) if usage else None
26
+ return {
27
+ "answer": choice.message.content or "",
28
+ "provider": "azure", "model_requested": deployment,
29
+ "model_returned": response.model,
30
+ "finish_reason": choice.finish_reason,
31
+ "usage": {
32
+ "input_tokens": usage.prompt_tokens if usage else None,
33
+ "output_tokens": usage.completion_tokens if usage else None,
34
+ "total_tokens": usage.total_tokens if usage else None,
35
+ "cached_input_tokens": getattr(details, "cached_tokens", None),
36
+ },
37
+ }