nikeshn commited on
Commit
e711654
Β·
verified Β·
1 Parent(s): 695ed61

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +40 -151
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
- # Inject UW-Madison repos first if institution is UW-Madison
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}", knowledge_section
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
- async def stream_response() -> AsyncGenerator[str, None]:
949
- full_text = ""
950
- try:
951
- # ── Call 1: Stream the answer with optional web search ──────────
952
- # Try with web search first, fall back to plain if not supported
 
 
 
 
 
 
 
953
  try:
954
- response = await client.chat.completions.create(
955
- model=MODEL,
956
- messages=messages,
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
- # Fallback: plain streaming without web search
965
- response = await client.chat.completions.create(
966
- model=MODEL,
967
- messages=messages,
968
- max_tokens=1200,
969
- temperature=0.4,
970
- stream=True,
971
- )
972
 
973
- async for chunk in response:
974
- token = chunk.choices[0].delta.content if chunk.choices else None
975
- if token:
976
- full_text += token
977
- yield f'data: {json.dumps({"token": token, "done": False})}\n\n'
978
-
979
- # ── Call 2: Classify route (small, fast, non-streaming) ─────────
980
- route = None
981
- prefill = {}
982
- try:
983
- classification_messages = [
984
- {
985
- "role": "system",
986
- "content": (
987
- "You are a routing classifier. Given a user message, "
988
- "determine if it needs one of these tools:\n"
989
- "- 'journals': user wants to find/recommend a journal, browse by subject, or analyse a manuscript\n"
990
- "- 'license': user wants to check self-archiving rights, deposit a paper, check embargo or OA policy for a specific journal\n"
991
- "- 'data': user wants to find a data repository or deposit a dataset\n"
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": full_text.strip(),
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