Spaces:
Running
Running
Upload app.py
Browse files
app.py
CHANGED
|
@@ -669,72 +669,7 @@ async def find_repository(req: RepoRequest, request: Request):
|
|
| 669 |
"fairsharing": f"https://fairsharing.org/search?q={clean_name}",
|
| 670 |
}
|
| 671 |
|
| 672 |
-
#
|
| 673 |
-
institution = (req.dataset.institution or "").lower()
|
| 674 |
-
is_uw = any(k in institution for k in ["wisconsin", "uw-madison", "uw madison", "uwmadison"])
|
| 675 |
-
if is_uw:
|
| 676 |
-
uw_repos = [
|
| 677 |
-
{
|
| 678 |
-
"name": "Dryad",
|
| 679 |
-
"type": "Generalist",
|
| 680 |
-
"url": "https://datadryad.org/stash",
|
| 681 |
-
"scope": "Open-access data repository for research data across all disciplines.",
|
| 682 |
-
"fit_reason": "Free for UW-Madison researchers through the institutional membership. Provides DOI, long-term stewardship, and publisher integrations.",
|
| 683 |
-
"data_types_accepted": "All types β tabular, images, genomics, climate, social science, and more",
|
| 684 |
-
"max_file_size": "300GB per deposit",
|
| 685 |
-
"licences_supported": "CC0 only",
|
| 686 |
-
"persistent_identifier": "DOI",
|
| 687 |
-
"versioning": "Yes",
|
| 688 |
-
"embargo_support": "No",
|
| 689 |
-
"access_model": "Open",
|
| 690 |
-
"cost": "Free (UW-Madison institutional membership)",
|
| 691 |
-
"certification": "None",
|
| 692 |
-
"fair_alignment": "High",
|
| 693 |
-
"funder_compliance_note": "Meets NSF, NIH, and most publisher data sharing mandates. UW-Madison membership covers all costs.",
|
| 694 |
-
"sensitive_data_suitable": "No β open access only, no PII or sensitive data",
|
| 695 |
-
"confidence": "High",
|
| 696 |
-
"verification_status": "Confirmed β UW-Madison institutional repository",
|
| 697 |
-
"risk_flag": None,
|
| 698 |
-
"verify_links": {
|
| 699 |
-
"re3data": "https://www.re3data.org/repository/r3d100000044",
|
| 700 |
-
"fairsharing": "https://fairsharing.org/FAIRsharing.wkggtx",
|
| 701 |
-
},
|
| 702 |
-
"uw_note": "UW-Madison institutional member β free to deposit. Requires ORCID iD and NetID.",
|
| 703 |
-
},
|
| 704 |
-
{
|
| 705 |
-
"name": "MINDS@UW",
|
| 706 |
-
"type": "Institutional",
|
| 707 |
-
"url": "https://minds.wisconsin.edu",
|
| 708 |
-
"scope": "UW-Madison's institutional repository for research papers, datasets, theses, preprints and digital materials.",
|
| 709 |
-
"fit_reason": "UW-Madison's own institutional repository β ideal for larger datasets or combining publications and data in one collection. Contact the team first to set up your collection.",
|
| 710 |
-
"data_types_accepted": "All types β datasets, articles, theses, reports, preprints, born-digital materials",
|
| 711 |
-
"max_file_size": "Case-by-case β good for large datasets exceeding Dryad's 300GB limit",
|
| 712 |
-
"licences_supported": "Open access (contact team for specifics)",
|
| 713 |
-
"persistent_identifier": "DOI",
|
| 714 |
-
"versioning": "Yes",
|
| 715 |
-
"embargo_support": "Yes",
|
| 716 |
-
"access_model": "Open",
|
| 717 |
-
"cost": "Free (UW-Madison affiliation required)",
|
| 718 |
-
"certification": "Institutional",
|
| 719 |
-
"fair_alignment": "High",
|
| 720 |
-
"funder_compliance_note": "Satisfies UW-Madison institutional OA and data management requirements.",
|
| 721 |
-
"sensitive_data_suitable": "No β open access only, no PII or sensitive data",
|
| 722 |
-
"confidence": "High",
|
| 723 |
-
"verification_status": "Confirmed β UW-Madison institutional repository",
|
| 724 |
-
"risk_flag": None,
|
| 725 |
-
"verify_links": {
|
| 726 |
-
"re3data": "https://www.re3data.org/search?query=MINDS+UW+Madison",
|
| 727 |
-
"fairsharing": "https://fairsharing.org/search?q=MINDS+UW+Madison",
|
| 728 |
-
},
|
| 729 |
-
"uw_note": "Contact minds.wisconsin.edu/contact to get started. Team will guide setup and deposit.",
|
| 730 |
-
},
|
| 731 |
-
]
|
| 732 |
-
# Remove any LLM-suggested Dryad/MINDS duplicates, then prepend UW repos
|
| 733 |
-
repos = [r for r in repos if not any(
|
| 734 |
-
uw in r.get("name", "").lower() for uw in ["dryad", "minds@uw", "minds@"]
|
| 735 |
-
)]
|
| 736 |
-
repos = uw_repos + repos
|
| 737 |
-
|
| 738 |
parsed["repositories"] = repos
|
| 739 |
return {"result": parsed}
|
| 740 |
|
|
@@ -915,26 +850,8 @@ async def chat(req: ChatRequest, request: Request):
|
|
| 915 |
# ββ Layer 3: LLM streaming ββββββββββββββββββββββββββββββββββββββββββββ
|
| 916 |
client = get_client()
|
| 917 |
|
| 918 |
-
# Only inject full UW knowledge when question is UW/OA-institution related
|
| 919 |
-
# For general questions keep system prompt lean to avoid token overflow
|
| 920 |
-
uw_triggers = [
|
| 921 |
-
"uw", "wisconsin", "madison", "dryad", "minds", "apc",
|
| 922 |
-
"article processing", "publishing fee", "waiver", "agreement",
|
| 923 |
-
"support oa", "cover the fee", "pay for", "free to publish",
|
| 924 |
-
"carrie nelson", "library support"
|
| 925 |
-
]
|
| 926 |
-
last_lower = last_msg.lower()
|
| 927 |
-
inject_knowledge = any(t in last_lower for t in uw_triggers)
|
| 928 |
-
|
| 929 |
-
knowledge_section = get_knowledge() if inject_knowledge else (
|
| 930 |
-
"UW-Madison has APC agreements with 13 publishers (ACS, ACM, Cambridge, Springer hybrid, "
|
| 931 |
-
"Wiley, IOP, ASM, Biochemical Society, Canadian Science Publishing, De Gruyter Brill, "
|
| 932 |
-
"Microbiology Society, PLoS, SCOAP3). Dryad and MINDS@UW are free institutional repositories. "
|
| 933 |
-
"For details: library.wisc.edu/research-support/scholarly-communication/open-access/publishing-support"
|
| 934 |
-
)
|
| 935 |
-
|
| 936 |
system = CHAT_SYSTEM_PROMPT_BASE.replace(
|
| 937 |
-
"{KU_KNOWLEDGE}",
|
| 938 |
).replace(
|
| 939 |
"{OPF_DATA}", opf_data if opf_data else "No Open Policy Finder data retrieved for this query."
|
| 940 |
)
|
|
@@ -945,80 +862,52 @@ async def chat(req: ChatRequest, request: Request):
|
|
| 945 |
messages = [{"role": "system", "content": system}]
|
| 946 |
messages += [{"role": m.role, "content": m.content} for m in req.messages]
|
| 947 |
|
| 948 |
-
|
| 949 |
-
|
| 950 |
-
|
| 951 |
-
|
| 952 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 953 |
try:
|
| 954 |
-
|
| 955 |
-
|
| 956 |
-
|
| 957 |
-
max_tokens=1200,
|
| 958 |
-
temperature=0.4,
|
| 959 |
-
stream=True,
|
| 960 |
-
tools=[{"type": "web_search_preview"}],
|
| 961 |
-
tool_choice="auto",
|
| 962 |
-
)
|
| 963 |
except Exception:
|
| 964 |
-
|
| 965 |
-
|
| 966 |
-
model=MODEL,
|
| 967 |
-
messages=messages,
|
| 968 |
-
max_tokens=1200,
|
| 969 |
-
temperature=0.4,
|
| 970 |
-
stream=True,
|
| 971 |
-
)
|
| 972 |
|
| 973 |
-
|
| 974 |
-
|
| 975 |
-
|
| 976 |
-
|
| 977 |
-
|
| 978 |
-
|
| 979 |
-
|
| 980 |
-
|
| 981 |
-
|
| 982 |
-
|
| 983 |
-
|
| 984 |
-
|
| 985 |
-
|
| 986 |
-
|
| 987 |
-
|
| 988 |
-
|
| 989 |
-
|
| 990 |
-
|
| 991 |
-
|
| 992 |
-
"- null: general question, greeting, concept explanation β no tool needed\n\n"
|
| 993 |
-
"Reply with ONLY valid JSON: "
|
| 994 |
-
'{\"route\": \"journals\"|\"license\"|\"data\"|null, \"prefill\": {}}\n'
|
| 995 |
-
"No other text. No explanation."
|
| 996 |
-
)
|
| 997 |
-
},
|
| 998 |
-
{"role": "user", "content": last_msg}
|
| 999 |
-
]
|
| 1000 |
-
route_resp = await client.chat.completions.create(
|
| 1001 |
-
model="gpt-4o-mini",
|
| 1002 |
-
messages=classification_messages,
|
| 1003 |
-
max_tokens=60,
|
| 1004 |
-
temperature=0.0,
|
| 1005 |
-
)
|
| 1006 |
-
route_text = route_resp.choices[0].message.content.strip()
|
| 1007 |
-
parsed_route = json.loads(route_text)
|
| 1008 |
-
candidate = parsed_route.get("route")
|
| 1009 |
-
if candidate in ("journals", "license", "data"):
|
| 1010 |
-
route = candidate
|
| 1011 |
-
prefill = parsed_route.get("prefill") or {}
|
| 1012 |
-
except Exception as re:
|
| 1013 |
-
logger.warning(f"[Chat route classification] {re}")
|
| 1014 |
route = None
|
| 1015 |
-
prefill = {}
|
| 1016 |
|
| 1017 |
done_payload = json.dumps({
|
| 1018 |
"token": "", "done": True,
|
| 1019 |
-
"reply":
|
| 1020 |
"route": route,
|
| 1021 |
-
"prefill": prefill,
|
| 1022 |
})
|
| 1023 |
yield f"data: {done_payload}\n\n"
|
| 1024 |
|
|
|
|
| 669 |
"fairsharing": f"https://fairsharing.org/search?q={clean_name}",
|
| 670 |
}
|
| 671 |
|
| 672 |
+
# Khazna is always prepended above
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 673 |
parsed["repositories"] = repos
|
| 674 |
return {"result": parsed}
|
| 675 |
|
|
|
|
| 850 |
# ββ Layer 3: LLM streaming ββββββββββββββββββββββββββββββββββββββββββββ
|
| 851 |
client = get_client()
|
| 852 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 853 |
system = CHAT_SYSTEM_PROMPT_BASE.replace(
|
| 854 |
+
"{KU_KNOWLEDGE}", get_knowledge()
|
| 855 |
).replace(
|
| 856 |
"{OPF_DATA}", opf_data if opf_data else "No Open Policy Finder data retrieved for this query."
|
| 857 |
)
|
|
|
|
| 862 |
messages = [{"role": "system", "content": system}]
|
| 863 |
messages += [{"role": m.role, "content": m.content} for m in req.messages]
|
| 864 |
|
| 865 |
+
def extract_route(text: str):
|
| 866 |
+
"""Brace-counting extraction of routing JSON from end of reply."""
|
| 867 |
+
starts = [m.start() for m in re.finditer(r'\{', text)]
|
| 868 |
+
for start in reversed(starts):
|
| 869 |
+
candidate = text[start:]
|
| 870 |
+
depth = 0; end = None
|
| 871 |
+
for i, ch in enumerate(candidate):
|
| 872 |
+
if ch == '{': depth += 1
|
| 873 |
+
elif ch == '}':
|
| 874 |
+
depth -= 1
|
| 875 |
+
if depth == 0: end = i + 1; break
|
| 876 |
+
if end is None: continue
|
| 877 |
try:
|
| 878 |
+
parsed = json.loads(candidate[:end])
|
| 879 |
+
if "route" in parsed:
|
| 880 |
+
return text[:start].strip(), parsed.get("route"), parsed.get("prefill", {})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 881 |
except Exception:
|
| 882 |
+
continue
|
| 883 |
+
return text.strip(), None, {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 884 |
|
| 885 |
+
async def stream_response() -> AsyncGenerator[str, None]:
|
| 886 |
+
full_text = ""
|
| 887 |
+
try:
|
| 888 |
+
# Plain streaming β no tool calls
|
| 889 |
+
async with client.chat.completions.stream(
|
| 890 |
+
model=MODEL,
|
| 891 |
+
messages=messages,
|
| 892 |
+
max_tokens=600,
|
| 893 |
+
temperature=0.3,
|
| 894 |
+
) as stream:
|
| 895 |
+
async for chunk in stream:
|
| 896 |
+
token = chunk.choices[0].delta.content if chunk.choices else None
|
| 897 |
+
if token:
|
| 898 |
+
full_text += token
|
| 899 |
+
yield f'data: {json.dumps({"token": token, "done": False})}\n\n'
|
| 900 |
+
|
| 901 |
+
# Extract route from reply using brace-counting
|
| 902 |
+
reply, route, prefill = extract_route(full_text)
|
| 903 |
+
if route not in ("journals", "license", "data", None):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 904 |
route = None
|
|
|
|
| 905 |
|
| 906 |
done_payload = json.dumps({
|
| 907 |
"token": "", "done": True,
|
| 908 |
+
"reply": reply,
|
| 909 |
"route": route,
|
| 910 |
+
"prefill": prefill or {},
|
| 911 |
})
|
| 912 |
yield f"data: {done_payload}\n\n"
|
| 913 |
|