-
Notifications
You must be signed in to change notification settings - Fork 2
Answer generation using pubmed references to obtain relevant publications #51
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,273 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Answer questions: vector search → expand via references → pick closest refs. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| For each question: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 1. Vector-search top-k documents (default 10) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 2. For each hit, get its references (pmid→pmcid), fetch their embeddings from Cosmos | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 3. Pick the ref_k references closest to the query vector (default 2) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 4. Combine: up to k + k*ref_k documents (e.g. 10 + 20 = 30) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 5. Answer with LLM | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Usage: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| python answer_questions_ref.py --config config.pubmed.yaml \ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| --questions complex_questions.json --output answers_refs.json \ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| --top 10 --ref-k 2 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import argparse, csv, json, logging, sys, time | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import numpy as np | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import yaml | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from azure.cosmos import CosmosClient | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from azure.identity import AzureCliCredential, get_bearer_token_provider | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from openai import AzureOpenAI | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from tqdm import tqdm | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| log = logging.getLogger(__name__) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for n in ("azure.core.pipeline.policies.http_logging_policy", "azure.identity", "azure.cosmos"): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logging.getLogger(n).setLevel(logging.WARNING) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def load_pmid_to_pmcid(csv_path: str) -> dict[str, str]: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mapping = {} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| with open(csv_path, newline="", encoding="utf-8") as f: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for row in csv.DictReader(f, quotechar='"'): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pmcid = (row.get("PMCID") or "").strip() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pmid = (row.get("PMID") or "").strip() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if pmcid and pmid: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mapping[pmid] = pmcid | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return mapping | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def get_embedding(text: str, embed_client: AzureOpenAI, model: str, dimensions: int, max_input_chars: int = 8000) -> list[float]: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return embed_client.embeddings.create( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| input=text[:max_input_chars], model=model, dimensions=dimensions | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ).data[0].embedding | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def vector_search(container, query_vector: list[float], top: int) -> list[dict]: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sql = """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| SELECT TOP @top | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| c.id, c.pmcid, c.title, c.journal_title, c.abstract, c.pub_year, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| c.doi, c.full_text, c.embedding, c.references, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| VectorDistance(c.embedding, @embedding) AS similarity | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| VectorDistance(c.embedding, @embedding) AS similarity | |
| VectorDistance(c.embedding, @embedding) AS distance |
Copilot
AI
Apr 23, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fetch_docs_by_pmcids pulls full_text (and other large fields) for every referenced PMCID, even though the selection step only needs embeddings to compute similarity. This can dramatically increase RU consumption and latency. Consider fetching only pmcid + embedding for candidate refs, then fetching full metadata/full_text only for the selected ref_k docs.
Copilot
AI
Apr 23, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
llm_model and token_scope are read with .get(...), so an empty string in config will override the intended defaults and later cause Azure OpenAI calls/token acquisition to fail (e.g., model="" or empty scope). Consider using llm_cfg.get(... ) or <default> and/or validating required settings (endpoint, deployment/model, token scope) early with a clear error.
| llm_endpoint = llm_cfg.get("llm_endpoint", "") | |
| llm_model = llm_cfg.get("llm_model", "gpt-5.4") | |
| llm_api_version = llm_cfg.get("api_version", "2024-12-01-preview") | |
| llm_token_scope = llm_cfg.get("token_scope", "https://cognitiveservices.azure.com/.default") | |
| llm_temperature = float(llm_cfg.get("temperature", 0.0)) | |
| llm_max_tokens = int(llm_cfg.get("max_completion_tokens", 4096)) | |
| llm_max_retries = int(llm_cfg.get("max_retries", 5)) | |
| embed_endpoint = embed_cfg.get("embed_endpoint", "") | |
| embed_api_key = embed_cfg.get("embed_api_key", "") | |
| embed_api_version = embed_cfg.get("api_version", "2024-12-01-preview") | |
| embed_model = embed_cfg.get("embed_model", "text-embedding-3-small") | |
| embed_dims = int(embed_cfg.get("embed_dimensions", 1536)) | |
| embed_max_input_chars = int(embed_cfg.get("max_input_chars", 8000)) | |
| pmc_ids_csv = answer_cfg.get("pmc_ids_csv", "") | |
| max_context_chars = int(answer_cfg.get("max_context_chars", 230000)) | |
| system_prompt = answer_cfg.get("system_prompt", "You are a biomedical research assistant.").strip() | |
| llm_endpoint = (llm_cfg.get("llm_endpoint") or "").strip() | |
| llm_model = llm_cfg.get("llm_model") or "gpt-5.4" | |
| llm_api_version = llm_cfg.get("api_version") or "2024-12-01-preview" | |
| llm_token_scope = llm_cfg.get("token_scope") or "https://cognitiveservices.azure.com/.default" | |
| llm_temperature = float(llm_cfg.get("temperature", 0.0)) | |
| llm_max_tokens = int(llm_cfg.get("max_completion_tokens", 4096)) | |
| llm_max_retries = int(llm_cfg.get("max_retries", 5)) | |
| embed_endpoint = (embed_cfg.get("embed_endpoint") or "").strip() | |
| embed_api_key = embed_cfg.get("embed_api_key", "") | |
| embed_api_version = embed_cfg.get("api_version") or "2024-12-01-preview" | |
| embed_model = embed_cfg.get("embed_model") or "text-embedding-3-small" | |
| embed_dims = int(embed_cfg.get("embed_dimensions", 1536)) | |
| embed_max_input_chars = int(embed_cfg.get("max_input_chars", 8000)) | |
| pmc_ids_csv = answer_cfg.get("pmc_ids_csv", "") | |
| max_context_chars = int(answer_cfg.get("max_context_chars", 230000)) | |
| system_prompt = (answer_cfg.get("system_prompt") or "You are a biomedical research assistant.").strip() | |
| if not llm_endpoint: | |
| raise ValueError("Missing required LLM configuration: llm_endpoint") | |
| if not llm_model: | |
| raise ValueError("Missing required LLM configuration: llm_model") | |
| if not llm_token_scope: | |
| raise ValueError("Missing required LLM configuration: token_scope") | |
| if not embed_endpoint: | |
| raise ValueError("Missing required embedding configuration: embed_endpoint") |
Copilot
AI
Apr 23, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The embedding client is always created with api_key=embed_api_key even though the repo config supports embedding.use_rbac_auth. This will break when RBAC is enabled (and embed_api_key is empty), and it’s inconsistent with how other code paths authenticate embeddings. Use azure_ad_token_provider when embedding.use_rbac_auth is true, otherwise require embed_api_key.
Copilot
AI
Apr 23, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For each of the top_k hits, this fetches all referenced PMCIDs from Cosmos before selecting ref_k closest. Since papers can have large reference lists, this can fan out into very large IN (...) queries and many RU-heavy reads per question. Consider capping the number of references considered per hit (or globally), deduplicating across hits before fetching, and/or adding a hard limit like max_ref_candidates in config.
| # 2. For each hit, get reference PMCIDs, fetch them, pick ref_k closest to query | |
| ref_docs_all = [] | |
| if ref_k > 0: | |
| for doc in hits: | |
| refs = doc.get("references") or [] | |
| ref_pmcids = [p for p in refs_to_pmcids(refs, pmid_map) if p not in seen_pmcids] | |
| if not ref_pmcids: | |
| continue | |
| fetched = fetch_docs_by_pmcids(container, ref_pmcids) | |
| if not fetched: | |
| continue | |
| selected = closest_select(fetched, query_vec, ref_k) | |
| # 2. For each hit, get reference PMCIDs, fetch a bounded set, pick ref_k closest to query | |
| ref_docs_all = [] | |
| fetched_ref_doc_cache = {} | |
| max_ref_candidates_per_hit = max(ref_k * 10, ref_k) | |
| if ref_k > 0: | |
| for doc in hits: | |
| refs = doc.get("references") or [] | |
| ref_pmcids = [] | |
| ref_pmcids_seen_for_hit = set() | |
| for pmcid in refs_to_pmcids(refs, pmid_map): | |
| if pmcid in seen_pmcids or pmcid in ref_pmcids_seen_for_hit: | |
| continue | |
| ref_pmcids_seen_for_hit.add(pmcid) | |
| ref_pmcids.append(pmcid) | |
| if len(ref_pmcids) >= max_ref_candidates_per_hit: | |
| break | |
| if not ref_pmcids: | |
| continue | |
| cached = [fetched_ref_doc_cache[pmcid] for pmcid in ref_pmcids if pmcid in fetched_ref_doc_cache] | |
| missing_pmcids = [pmcid for pmcid in ref_pmcids if pmcid not in fetched_ref_doc_cache] | |
| fetched = fetch_docs_by_pmcids(container, missing_pmcids) if missing_pmcids else [] | |
| for fetched_doc in fetched: | |
| fetched_pmcid = fetched_doc.get("pmcid") | |
| if fetched_pmcid: | |
| fetched_ref_doc_cache[fetched_pmcid] = fetched_doc | |
| candidates = cached + fetched | |
| if not candidates: | |
| continue | |
| selected = closest_select(candidates, query_vec, ref_k) |
Copilot
AI
Apr 23, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Determining source via "vector" if d in hits else "reference" relies on full dict equality and can misclassify documents if dict contents differ (or if a ref doc happens to compare equal). Track source explicitly (e.g., add a source field when constructing hits / ref_docs_all, or compare by a stable key like pmcid).
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -54,11 +54,11 @@ paths: | |||||||||
| # --------------------------------------------------------------------------- | ||||||||||
| llm: | ||||||||||
| llm_endpoint: "" | ||||||||||
| api_version: "2024-05-01-preview" | ||||||||||
| llm_model: "gpt-5.4-mini" | ||||||||||
| api_version: "2024-12-01-preview" | ||||||||||
| llm_model: "" | ||||||||||
|
||||||||||
| llm_model: "" | |
| # Leave llm_model unset here so code that uses .get("llm_model", <default>) | |
| # can fall back to its default deployment name, or validation can clearly | |
| # report that the setting is missing. |
Copilot
AI
Apr 23, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
answer.pmc_ids_csv points to data/PMC-ids.csv, but that file doesn’t appear to exist in the repo. Since answer_questions_ref.py tries to open this path when it’s non-empty, the default config will cause a FileNotFoundError at runtime. Consider either committing the CSV, pointing to the correct tracked location, or setting the default to empty and documenting it as optional.
| pmc_ids_csv: "data/PMC-ids.csv" | |
| pmc_ids_csv: "" # Optional: set to a tracked/local PMCID mapping CSV when available |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unused import:
sysis imported but never used. Please remove it to keep the script clean (and to satisfy linters if enabled).