初始提交

This commit is contained in:
potaser
2026-07-10 17:59:59 +08:00
commit 5bb98a937a
660 changed files with 871233 additions and 0 deletions
+70
View File
@@ -0,0 +1,70 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[1]
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
from src.rag.extractors.v16_1_2.paper_metadata_enricher_v16_1_2 import (
PaperMetadataEnricherV16,
)
def main() -> int:
parser = argparse.ArgumentParser(
description="Add canonical DOI/PMID/PMCID and citation metadata to a V16 result.json."
)
parser.add_argument("--result-json", required=True, type=Path)
parser.add_argument("--pdf", type=Path)
parser.add_argument("--doi", default="")
parser.add_argument("--pmid", default="")
parser.add_argument("--pmcid", default="")
parser.add_argument("--output", type=Path)
parser.add_argument("--in-place", action="store_true")
args = parser.parse_args()
source = args.result_json.resolve()
if not source.exists():
raise SystemExit(f"Missing result JSON: {source}")
if not args.in_place and not args.output:
raise SystemExit("Use --in-place or provide --output.")
destination = source if args.in_place else args.output.resolve()
data = json.loads(source.read_text(encoding="utf-8"))
enricher = PaperMetadataEnricherV16()
paper = enricher.resolve(
pdf_path=args.pdf.resolve() if args.pdf else None,
doi=args.doi,
pmid=args.pmid,
pmcid=args.pmcid,
)
enricher.enrich_output(data, paper)
destination.parent.mkdir(parents=True, exist_ok=True)
destination.write_text(
json.dumps(data, ensure_ascii=False, indent=2),
encoding="utf-8",
)
audit_path = destination.parent / "paper_metadata_enrichment_audit.json"
enricher.save_audit(audit_path, paper)
print(
json.dumps(
{
"result_json": str(destination),
"audit": str(audit_path),
"paper": paper,
"relations": len(data.get("relations", [])),
},
ensure_ascii=False,
indent=2,
)
)
return 0
if __name__ == "__main__":
raise SystemExit(main())
+405
View File
@@ -0,0 +1,405 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import csv
import hashlib
import html
import json
import re
import unicodedata
from collections import Counter
from difflib import SequenceMatcher
from pathlib import Path
from typing import Any
from ir_measures import P, RR, Recall, nDCG, Qrel, ScoredDoc, calc_aggregate
CONFIGS = [
"A0_full",
"A1_no_planner",
"A2_no_neo4j",
"A3_no_vector",
"A4_one_hop",
"A5_no_bge",
]
KS = (5, 10, 30)
def normalize(text: Any) -> str:
value = html.unescape(str(text or ""))
value = unicodedata.normalize("NFKC", value).lower()
value = re.sub(r"<[^>]+>", " ", value)
value = re.sub(r"https?://\S+", " ", value)
value = re.sub(r"[^a-z0-9\u4e00-\u9fff.%+-]+", " ", value)
return " ".join(value.split())
def tokens(text: Any) -> list[str]:
return re.findall(r"[a-z0-9.%+-]+|[\u4e00-\u9fff]", normalize(text))
def evidence_similarity(gold: str, candidate: str) -> dict[str, float | bool]:
ng = normalize(gold)
nc = normalize(candidate)
if not ng or not nc:
return {
"matched": False,
"containment": False,
"token_recall": 0.0,
"token_precision": 0.0,
"sequence_ratio": 0.0,
"match_score": 0.0,
}
containment = ng in nc or nc in ng
gt = set(tokens(gold))
ct = set(tokens(candidate))
overlap = len(gt & ct)
token_recall = overlap / len(gt) if gt else 0.0
token_precision = overlap / len(ct) if ct else 0.0
sequence_ratio = SequenceMatcher(None, ng, nc).ratio()
match_score = max(
sequence_ratio,
0.72 * token_recall + 0.28 * token_precision,
)
matched = bool(
containment
or sequence_ratio >= 0.82
or (token_recall >= 0.72 and token_precision >= 0.28)
)
return {
"matched": matched,
"containment": containment,
"token_recall": token_recall,
"token_precision": token_precision,
"sequence_ratio": sequence_ratio,
"match_score": match_score,
}
def evidence_doc_id(evidence: str) -> str:
digest = hashlib.sha1(normalize(evidence).encode("utf-8")).hexdigest()[:20]
return f"EVID::{digest}"
def paper_id(source: dict[str, Any]) -> str:
return str(
source.get("paper_id")
or source.get("pmcid")
or source.get("pmid")
or source.get("doi")
or source.get("paper_title")
or source.get("title")
or ""
).strip()
def dedupe_ranked_sources(
qid: str,
gold_source: str,
sources: list[dict[str, Any]],
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
ranked: list[dict[str, Any]] = []
audit: list[dict[str, Any]] = []
seen_evidence: set[str] = set()
gold_doc_id = f"GOLD::{qid}"
for raw_rank, source in enumerate(sources, 1):
evidence = str(source.get("evidence") or "")
normalized = normalize(evidence)
if not normalized or normalized in seen_evidence:
continue
seen_evidence.add(normalized)
similarity = evidence_similarity(gold_source, evidence)
doc_id = gold_doc_id if similarity["matched"] else evidence_doc_id(evidence)
row = {
"rank": len(ranked) + 1,
"raw_rank": raw_rank,
"doc_id": doc_id,
"paper_id": paper_id(source),
"evidence": evidence,
"source": source.get("source", ""),
"relation": source.get("relation", ""),
"target": source.get("target", ""),
**similarity,
}
ranked.append(row)
audit.append(row)
return ranked, audit
def metric_key(measure: Any) -> str:
return str(measure)
def write_csv(path: Path, rows: list[dict[str, Any]]) -> None:
if not rows:
return
with path.open("w", encoding="utf-8-sig", newline="") as handle:
writer = csv.DictWriter(handle, fieldnames=list(rows[0]))
writer.writeheader()
writer.writerows(rows)
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--results-dir", required=True)
parser.add_argument("--questions", required=True)
parser.add_argument("--judgments", required=True)
parser.add_argument("--output-dir", required=True)
args = parser.parse_args()
results_dir = Path(args.results_dir)
questions = json.loads(Path(args.questions).read_text(encoding="utf-8-sig"))
question_by_id = {str(item["id"]): item for item in questions}
judgment_rows = list(
csv.DictReader(open(args.judgments, encoding="utf-8-sig"))
)
judgment_by_key = {
(row["config_id"], row["question_id"]): row for row in judgment_rows
}
output_dir = Path(args.output_dir)
output_dir.mkdir(parents=True, exist_ok=True)
(output_dir / "trec_runs").mkdir(exist_ok=True)
qrels = [
Qrel(str(item["id"]), f"GOLD::{item['id']}", 1) for item in questions
]
qrels_path = output_dir / "known_item_qrels.tsv"
with qrels_path.open("w", encoding="utf-8", newline="") as handle:
for item in questions:
qid = str(item["id"])
handle.write(f"{qid}\t0\tGOLD::{qid}\t1\n")
measures = [
P @ 5,
P @ 10,
P @ 30,
Recall @ 5,
Recall @ 10,
Recall @ 30,
RR @ 30,
nDCG @ 5,
nDCG @ 10,
nDCG @ 30,
]
summary_rows: list[dict[str, Any]] = []
query_rows: list[dict[str, Any]] = []
match_audit: list[dict[str, Any]] = []
for config in CONFIGS:
run: list[ScoredDoc] = []
diversity: dict[int, list[float]] = {k: [] for k in KS}
diversity_returned: dict[int, list[float]] = {k: [] for k in KS}
duplicate_rates: list[float] = []
retrieved_counts: list[int] = []
per_question_ranked: dict[str, list[dict[str, Any]]] = {}
run_path = output_dir / "trec_runs" / f"{config}.run"
with run_path.open("w", encoding="utf-8", newline="") as run_handle:
for item in questions:
qid = str(item["id"])
record = json.loads(
(results_dir / config / f"{qid}.json").read_text(
encoding="utf-8"
)
)
response = record.get("response") or {}
raw_sources = response.get("sources") or []
ranked, audit = dedupe_ranked_sources(
qid, str(item.get("source_text") or ""), raw_sources
)
per_question_ranked[qid] = ranked
retrieved_counts.append(len(ranked))
duplicate_rates.append(
1 - (len(ranked) / len(raw_sources)) if raw_sources else 0.0
)
for row in audit:
match_audit.append(
{
"config_id": config,
"question_id": qid,
"gold_source_text": item.get("source_text", ""),
**row,
}
)
for row in ranked:
score = 10000.0 - float(row["rank"])
run.append(ScoredDoc(qid, str(row["doc_id"]), score))
run_handle.write(
f"{qid} Q0 {row['doc_id']} {row['rank']} "
f"{score:.6f} {config}\n"
)
for k in KS:
top = ranked[:k]
unique_papers = len(
{str(row["paper_id"]) for row in top if row["paper_id"]}
)
diversity[k].append(unique_papers / k)
diversity_returned[k].append(
unique_papers / len(top) if top else 0.0
)
aggregate = calc_aggregate(measures, qrels, run)
summary: dict[str, Any] = {
"config_id": config,
"questions": len(questions),
}
for measure in measures:
summary[metric_key(measure)] = float(aggregate[measure])
for k in KS:
summary[f"source_diversity@{k}"] = sum(diversity[k]) / len(
diversity[k]
)
summary[f"source_diversity_returned@{k}"] = sum(
diversity_returned[k]
) / len(diversity_returned[k])
summary["mean_exact_duplicate_rate"] = sum(duplicate_rates) / len(
duplicate_rates
)
summary["mean_deduplicated_source_count"] = sum(retrieved_counts) / len(
retrieved_counts
)
answer_counts = Counter()
answer_scores: list[float] = []
for item in questions:
qid = str(item["id"])
judgment = judgment_by_key[(config, qid)]
verdict = judgment["answer_verdict"]
answer_counts[verdict] += 1
answer_scores.append(
{"correct": 1.0, "partial": 0.5}.get(verdict, 0.0)
)
ranked = per_question_ranked[qid]
first_rank = next(
(
int(row["rank"])
for row in ranked
if row["doc_id"] == f"GOLD::{qid}"
),
0,
)
query_rows.append(
{
"config_id": config,
"question_id": qid,
"question": item["question"],
"answer_verdict": verdict,
"answer_score": {
"correct": 1.0,
"partial": 0.5,
}.get(verdict, 0.0),
"evidence_verdict_llm": judgment["evidence_verdict"],
"known_gold_first_rank": first_rank,
"known_gold_hit@5": int(bool(first_rank and first_rank <= 5)),
"known_gold_hit@10": int(
bool(first_rank and first_rank <= 10)
),
"known_gold_hit@30": int(
bool(first_rank and first_rank <= 30)
),
"deduplicated_source_count": len(ranked),
"unique_papers@10": len(
{
str(row["paper_id"])
for row in ranked[:10]
if row["paper_id"]
}
),
"judge_reason": judgment["reason"],
}
)
summary["answer_correct"] = answer_counts["correct"]
summary["answer_partial"] = answer_counts["partial"]
summary["answer_wrong"] = answer_counts["wrong"]
summary["answer_insufficient"] = answer_counts["insufficient"]
summary["answer_correctness_1_0.5_0"] = sum(answer_scores) / len(
answer_scores
)
summary_rows.append(summary)
scored_rows: list[dict[str, Any]] = []
for item in questions:
qid = str(item["id"])
judgment = judgment_by_key[(config, qid)]
verdict = judgment["answer_verdict"]
record = json.loads(
(results_dir / config / f"{qid}.json").read_text(
encoding="utf-8"
)
)
scored_rows.append(
{
"id": qid,
"question": item["question"],
"gold_answer": item["gold_answer"],
"deepseek_answer": (record.get("response") or {}).get(
"answer", ""
),
"score": {"correct": 1.0, "partial": 0.5}.get(
verdict, 0.0
),
"label": verdict,
"reason": judgment["reason"],
"model": "DeepSeek V4Flash + RAG",
"status": "answered",
}
)
config_base = output_dir / f"{config}_same_scale_correctness"
(config_base.with_suffix(".json")).write_text(
json.dumps(scored_rows, ensure_ascii=False, indent=2),
encoding="utf-8",
)
write_csv(config_base.with_suffix(".csv"), scored_rows)
config_summary = {
"model": f"DeepSeek V4Flash + RAG ({config})",
"n": len(scored_rows),
"total_score": sum(float(row["score"]) for row in scored_rows),
"answer_correctness": summary["answer_correctness_1_0.5_0"],
"label_counts": dict(answer_counts),
"score_counts": dict(
Counter(str(row["score"]) for row in scored_rows)
),
"scoring_note": (
"Unified 1/0.5/0 scale. Labels come from the calibrated "
"DeepSeek evidence-aware judge, not hard-coded question IDs."
),
}
(output_dir / f"{config}_same_scale_correctness_summary.json").write_text(
json.dumps(config_summary, ensure_ascii=False, indent=2),
encoding="utf-8",
)
write_csv(output_dir / "ir_metrics_summary.csv", summary_rows)
write_csv(output_dir / "ir_metrics_per_question.csv", query_rows)
write_csv(output_dir / "gold_evidence_match_audit.csv", match_audit)
report = {
"protocol": "known-item retrieval evaluation",
"implementation": "ir_measures 0.4.1 / pytrec-eval-terrier",
"qrels": str(qrels_path),
"ks": KS,
"matching_rule": {
"containment": True,
"sequence_ratio_threshold": 0.82,
"token_recall_threshold": 0.72,
"token_precision_threshold": 0.28,
},
"important_limitation": (
"Each question has one known gold source text. Therefore Recall@k "
"is equivalent to known-item hit rate. Precision@k and nDCG@k "
"treat unjudged evidence as non-relevant and may underestimate "
"true retrieval quality. Formal publication-grade P/nDCG requires "
"pooled relevance judgments over retrieved evidence."
),
"summary": summary_rows,
}
(output_dir / "ir_evaluation_report.json").write_text(
json.dumps(report, ensure_ascii=False, indent=2),
encoding="utf-8",
)
print(json.dumps(report, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()
+371
View File
@@ -0,0 +1,371 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import csv
import json
import re
import time
from collections import Counter
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path
from typing import Any
import requests
from src.rag.project_config import env
CONFIGS = [
"A0_full",
"A1_no_planner",
"A2_no_neo4j",
"A3_no_vector",
"A4_one_hop",
"A5_no_bge",
]
def atomic_json(path: Path, payload: Any) -> None:
tmp = path.with_suffix(path.suffix + ".tmp")
tmp.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
tmp.replace(path)
def extract_json(text: str) -> dict[str, Any]:
text = (text or "").strip()
try:
return json.loads(text)
except Exception:
pass
fenced = re.search(r"```(?:json)?\s*(\{.*?\})\s*```", text, re.S)
if fenced:
try:
return json.loads(fenced.group(1))
except Exception:
pass
match = re.search(r"\{.*\}", text, re.S)
if match:
try:
return json.loads(match.group(0))
except Exception:
pass
return {}
def compact_sources(sources: list[dict[str, Any]]) -> list[dict[str, Any]]:
compact: list[dict[str, Any]] = []
seen: set[str] = set()
for source in sources:
evidence = " ".join(str(source.get("evidence") or "").split())
key = evidence.lower()
if not evidence or key in seen:
continue
seen.add(key)
compact.append(
{
"srt": (
f"{source.get('source','')} -- "
f"{source.get('relation','')} --> "
f"{source.get('target','')}"
),
"evidence": evidence[:260],
}
)
if len(compact) >= 5:
break
return compact
def judge_one(
*,
root: Path,
output_dir: Path,
gold: dict[str, Any],
api_url: str,
api_key: str,
model: str,
retries: int,
) -> dict[str, Any]:
qid = str(gold["id"])
output_path = output_dir / f"{qid}.json"
if output_path.exists():
try:
existing = json.loads(output_path.read_text(encoding="utf-8"))
judgments = existing.get("judgments") or []
if len(judgments) == len(CONFIGS):
return {"question_id": qid, "status": "ok", "resumed": True}
except Exception:
pass
candidates: list[dict[str, Any]] = []
for config in CONFIGS:
record = json.loads(
(root / config / f"{qid}.json").read_text(encoding="utf-8")
)
response = record.get("response") or {}
candidates.append(
{
"config_id": config,
"answer": response.get("answer", ""),
"retrieved_evidence": compact_sources(response.get("sources") or []),
}
)
prompt = f"""
You are evaluating six RAG answers to one plant-biology question.
The GOLD SOURCE TEXT is authoritative. The short GOLD ANSWER may be incomplete;
if they differ, judge against the GOLD SOURCE TEXT.
Do not reward verbosity. Do not penalize a concise correct answer.
Judge each system independently.
Question:
{gold['question']}
Gold answer:
{gold['gold_answer']}
Gold source text:
{gold['source_text']}
Candidate systems:
{json.dumps(candidates, ensure_ascii=False)}
For each configuration, judge:
1. answer_verdict:
- correct: all facts needed by the question are correct; extra claims do not conflict.
- partial: some required facts are correct but important requested facts are missing,
or the answer mixes the target result with conflicting alternatives.
- wrong: gives a conflicting or unrelated substantive answer.
- insufficient: explicitly cannot answer or gives no substantive answer.
2. evidence_verdict:
- full: retrieved evidence contains all facts needed to answer.
- partial: retrieved evidence contains some but not all required facts.
- miss: retrieved evidence does not contain the target fact.
3. usable: true only when answer_verdict is correct or partial.
Return JSON only:
{{
"question_id": "{qid}",
"judgments": [
{{
"config_id": "A0_full",
"answer_verdict": "correct|partial|wrong|insufficient",
"evidence_verdict": "full|partial|miss",
"usable": true,
"reason": "maximum 15 words"
}}
]
}}
""".strip()
last_error = ""
raw = ""
for attempt in range(1, retries + 2):
try:
response = requests.post(
api_url,
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
},
json={
"model": model,
"messages": [
{
"role": "system",
"content": "Return valid JSON only. Apply the evaluation rubric consistently.",
},
{"role": "user", "content": prompt},
],
"temperature": 0,
"max_tokens": 4000,
},
timeout=240,
)
response.raise_for_status()
data = response.json()
raw = (data.get("choices") or [{}])[0].get("message", {}).get(
"content", ""
)
parsed = extract_json(raw)
judgments = parsed.get("judgments") or []
config_ids = {str(item.get("config_id")) for item in judgments}
if len(judgments) == len(CONFIGS) and config_ids == set(CONFIGS):
payload = {
"question_id": qid,
"question": gold["question"],
"gold_answer": gold["gold_answer"],
"gold_source_text": gold["source_text"],
"judgments": judgments,
"attempt": attempt,
"model": model,
}
atomic_json(output_path, payload)
return {"question_id": qid, "status": "ok", "attempt": attempt}
last_error = "Incomplete or invalid judgment JSON"
except Exception as exc:
last_error = f"{type(exc).__name__}: {exc}"
time.sleep(min(4 * attempt, 12))
atomic_json(
output_path,
{
"question_id": qid,
"question": gold["question"],
"error": last_error,
"raw": raw,
"model": model,
},
)
return {"question_id": qid, "status": "error", "error": last_error}
def summarize(
root: Path,
judge_dir: Path,
questions: list[dict[str, Any]],
output_dir: Path,
) -> None:
rows: list[dict[str, Any]] = []
for gold in questions:
qid = str(gold["id"])
judged = json.loads((judge_dir / f"{qid}.json").read_text(encoding="utf-8"))
for item in judged.get("judgments") or []:
config = str(item["config_id"])
record = json.loads(
(root / config / f"{qid}.json").read_text(encoding="utf-8")
)
response = record.get("response") or {}
rows.append(
{
"question_id": qid,
"config_id": config,
"question": gold["question"],
"gold_answer": gold["gold_answer"],
"answer_verdict": item.get("answer_verdict", ""),
"evidence_verdict": item.get("evidence_verdict", ""),
"usable": bool(item.get("usable")),
"reason": item.get("reason", ""),
"answer": response.get("answer", ""),
"source_count": len(response.get("sources") or []),
"result_file": str(root / config / f"{qid}.json"),
}
)
csv_path = output_dir / "llm_judgments_100q.csv"
with csv_path.open("w", encoding="utf-8-sig", newline="") as handle:
writer = csv.DictWriter(handle, fieldnames=list(rows[0]))
writer.writeheader()
writer.writerows(rows)
summaries: list[dict[str, Any]] = []
for config in CONFIGS:
subset = [row for row in rows if row["config_id"] == config]
answers = Counter(str(row["answer_verdict"]) for row in subset)
evidence = Counter(str(row["evidence_verdict"]) for row in subset)
n = len(subset)
weighted_answer = (
answers["correct"] + 0.5 * answers["partial"]
) / n
weighted_recall = (
evidence["full"] + 0.5 * evidence["partial"]
) / n
summaries.append(
{
"config_id": config,
"questions": n,
"correct": answers["correct"],
"partial": answers["partial"],
"wrong": answers["wrong"],
"insufficient": answers["insufficient"],
"strict_answer_accuracy": answers["correct"] / n,
"weighted_answer_accuracy": weighted_answer,
"usable_answer_rate": sum(
1 for row in subset if row["usable"]
)
/ n,
"full_evidence_hits": evidence["full"],
"partial_evidence_hits": evidence["partial"],
"evidence_misses": evidence["miss"],
"question_level_full_recall": evidence["full"] / n,
"question_level_weighted_recall": weighted_recall,
}
)
summary = {
"evaluation": "DeepSeek LLM-assisted judge",
"warning": (
"These are automated judge estimates. They are not a substitute for "
"human review and should be calibrated against the manually reviewed 20 questions."
),
"configs": summaries,
}
atomic_json(output_dir / "llm_judgment_summary_100q.json", summary)
with (output_dir / "llm_judgment_summary_100q.csv").open(
"w", encoding="utf-8-sig", newline=""
) as handle:
writer = csv.DictWriter(handle, fieldnames=list(summaries[0]))
writer.writeheader()
writer.writerows(summaries)
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--input-dir", required=True)
parser.add_argument("--questions", required=True)
parser.add_argument("--output-dir", required=True)
parser.add_argument("--workers", type=int, default=4)
parser.add_argument("--retries", type=int, default=2)
args = parser.parse_args()
api_key = env("DEEPSEEK_API_KEY")
if not api_key:
raise RuntimeError("No DEEPSEEK_API_KEY configured")
base_url = env("DEEPSEEK_BASE_URL", "https://api.deepseek.com").rstrip("/")
model = env("V15_RELATION_LLM_MODEL", "deepseek-v4-flash")
questions = json.loads(Path(args.questions).read_text(encoding="utf-8-sig"))
root = Path(args.input_dir)
output_dir = Path(args.output_dir)
judge_dir = output_dir / "per_question"
judge_dir.mkdir(parents=True, exist_ok=True)
progress_path = output_dir / "judge_progress.json"
completed: list[dict[str, Any]] = []
with ThreadPoolExecutor(max_workers=max(1, args.workers)) as pool:
futures = [
pool.submit(
judge_one,
root=root,
output_dir=judge_dir,
gold=gold,
api_url=f"{base_url}/chat/completions",
api_key=api_key,
model=model,
retries=args.retries,
)
for gold in questions
]
for index, future in enumerate(as_completed(futures), 1):
result = future.result()
completed.append(result)
atomic_json(
progress_path,
{
"completed": index,
"total": len(questions),
"errors": [x for x in completed if x["status"] == "error"],
},
)
print(
f"[{index}/{len(questions)}] {result['question_id']} "
f"{result['status']}",
flush=True,
)
errors = [item for item in completed if item["status"] == "error"]
if errors:
raise RuntimeError(f"Judge failures: {errors}")
summarize(root, judge_dir, questions, output_dir)
if __name__ == "__main__":
main()
+230
View File
@@ -0,0 +1,230 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import csv
import json
import re
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path
from typing import Any
import requests
from src.rag.project_config import env
def atomic_json(path: Path, payload: Any) -> None:
tmp = path.with_suffix(path.suffix + ".tmp")
tmp.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
tmp.replace(path)
def extract_json(text: str) -> dict[str, Any]:
text = (text or "").strip()
try:
return json.loads(text)
except Exception:
pass
match = re.search(r"\{.*\}", text, re.S)
if match:
try:
return json.loads(match.group(0))
except Exception:
pass
return {}
def judge_one(
gold: dict[str, Any],
answer: dict[str, Any],
output_dir: Path,
api_url: str,
api_key: str,
model: str,
) -> dict[str, Any]:
qid = str(gold["id"])
output = output_dir / f"{qid}.json"
if output.exists():
try:
existing = json.loads(output.read_text(encoding="utf-8"))
if existing.get("answer_verdict"):
return {"question_id": qid, "status": "ok", "resumed": True}
except Exception:
pass
candidate = str(answer.get("deepseek_answer") or "")
prompt = f"""
Evaluate one direct-LLM answer to a plant-biology question.
The GOLD SOURCE TEXT is authoritative. The short GOLD ANSWER may be incomplete.
Do not reward verbosity. Judge only factual correctness and completeness.
Question:
{gold['question']}
Gold answer:
{gold['gold_answer']}
Gold source text:
{gold['source_text']}
Candidate answer:
{candidate}
Return one compact JSON object only:
{{"answer_verdict":"correct|partial|wrong|insufficient","usable":true,"reason":"maximum 12 words"}}
Definitions:
- correct: all facts needed by the question are correct.
- partial: some required facts are correct, but important requested facts are missing.
- wrong: substantive answer conflicts with the gold source text.
- insufficient: no substantive answer or explicitly cannot answer.
- usable is true only for correct or partial.
""".strip()
last_error = ""
for attempt in range(1, 4):
try:
response = requests.post(
api_url,
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
},
json={
"model": model,
"messages": [
{"role": "system", "content": "Return valid JSON only."},
{"role": "user", "content": prompt},
],
"temperature": 0,
"max_tokens": 1200,
},
timeout=180,
)
response.raise_for_status()
content = (response.json().get("choices") or [{}])[0].get(
"message", {}
).get("content", "")
parsed = extract_json(content)
verdict = parsed.get("answer_verdict")
if verdict in {"correct", "partial", "wrong", "insufficient"}:
payload = {
"question_id": qid,
"question": gold["question"],
"gold_answer": gold["gold_answer"],
"gold_source_text": gold["source_text"],
"deepseek_answer": candidate,
"answer_verdict": verdict,
"usable": bool(parsed.get("usable")),
"reason": parsed.get("reason", ""),
"attempt": attempt,
"model": model,
}
atomic_json(output, payload)
return {"question_id": qid, "status": "ok"}
last_error = "invalid verdict"
except Exception as exc:
last_error = f"{type(exc).__name__}: {exc}"
time.sleep(3 * attempt)
atomic_json(output, {"question_id": qid, "error": last_error})
return {"question_id": qid, "status": "error", "error": last_error}
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--answers", required=True)
parser.add_argument("--questions", required=True)
parser.add_argument("--output-dir", required=True)
parser.add_argument("--workers", type=int, default=4)
args = parser.parse_args()
api_key = env("DEEPSEEK_API_KEY")
if not api_key:
raise RuntimeError("No DEEPSEEK_API_KEY configured")
base_url = env("DEEPSEEK_BASE_URL", "https://api.deepseek.com").rstrip("/")
model = env("V15_RELATION_LLM_MODEL", "deepseek-v4-flash")
answers = json.loads(Path(args.answers).read_text(encoding="utf-8-sig"))
questions = json.loads(Path(args.questions).read_text(encoding="utf-8-sig"))
answer_by_id = {str(item["id"]): item for item in answers}
output_dir = Path(args.output_dir)
per_question = output_dir / "per_question"
per_question.mkdir(parents=True, exist_ok=True)
results: list[dict[str, Any]] = []
with ThreadPoolExecutor(max_workers=max(1, args.workers)) as pool:
futures = [
pool.submit(
judge_one,
gold,
answer_by_id[str(gold["id"])],
per_question,
f"{base_url}/chat/completions",
api_key,
model,
)
for gold in questions
]
for index, future in enumerate(as_completed(futures), 1):
result = future.result()
results.append(result)
print(
f"[{index}/{len(questions)}] {result['question_id']} "
f"{result['status']}",
flush=True,
)
errors = [item for item in results if item["status"] == "error"]
if errors:
raise RuntimeError(errors)
rows: list[dict[str, Any]] = []
for gold in questions:
qid = str(gold["id"])
judged = json.loads(
(per_question / f"{qid}.json").read_text(encoding="utf-8")
)
verdict = judged["answer_verdict"]
rows.append(
{
"id": qid,
"question": gold["question"],
"gold_answer": gold["gold_answer"],
"deepseek_answer": judged["deepseek_answer"],
"score": {"correct": 1.0, "partial": 0.5}.get(verdict, 0.0),
"label": verdict,
"reason": judged["reason"],
"model": "DeepSeek V4Flash direct, unified judge",
"status": "answered",
}
)
with (output_dir / "direct_same_scale_correctness.csv").open(
"w", encoding="utf-8-sig", newline=""
) as handle:
writer = csv.DictWriter(handle, fieldnames=list(rows[0]))
writer.writeheader()
writer.writerows(rows)
(output_dir / "direct_same_scale_correctness.json").write_text(
json.dumps(rows, ensure_ascii=False, indent=2),
encoding="utf-8",
)
counts: dict[str, int] = {}
for row in rows:
counts[row["label"]] = counts.get(row["label"], 0) + 1
summary = {
"model": "DeepSeek V4Flash direct, unified evidence-aware judge",
"n": len(rows),
"total_score": sum(float(row["score"]) for row in rows),
"answer_correctness": sum(float(row["score"]) for row in rows)
/ len(rows),
"label_counts": counts,
"scoring_note": (
"Same correct=1, partial=0.5, wrong/insufficient=0 rubric "
"used for the RAG ablation configurations."
),
}
atomic_json(output_dir / "direct_same_scale_correctness_summary.json", summary)
print(json.dumps(summary, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()
+131
View File
@@ -0,0 +1,131 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import json
import shutil
from pathlib import Path
CONFIGS = [
"A0_full",
"A1_no_planner",
"A2_no_neo4j",
"A3_no_vector",
"A4_one_hop",
"A5_no_bge",
]
PROJECT_ROOT = Path(__file__).resolve().parents[1]
def resolve_path(value: str | Path) -> Path:
path = Path(value)
if not path.is_absolute():
path = PROJECT_ROOT / path
return path
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument(
"--base-dir",
default="experiments/rag_ablation_20260624",
help="Base ablation experiment directory, relative to the project root unless absolute.",
)
parser.add_argument("--first-20-dir", default="")
parser.add_argument("--remaining-80-dir", default="")
parser.add_argument("--output-dir", default="")
parser.add_argument("--questions", default="")
parser.add_argument(
"--allow-existing-output",
action="store_true",
help="Allow writing into an existing output directory without deleting it.",
)
args = parser.parse_args()
base = resolve_path(args.base_dir)
first_20 = resolve_path(args.first_20_dir) if args.first_20_dir else base / "paper_module_strict_official_20q_7907_20260624"
remaining_80 = resolve_path(args.remaining_80_dir) if args.remaining_80_dir else base / "paper_module_remaining_80q_7907_20260624"
output = resolve_path(args.output_dir) if args.output_dir else base / "paper_module_strict_official_100q_7907_20260624"
questions_path = (
resolve_path(args.questions)
if args.questions
else base / "00_inputs" / "QA100_questions_gold_answers_source_text_no_srt_audited_v1_20260624.json"
)
questions = json.loads(questions_path.read_text(encoding="utf-8-sig"))
ids = [str(item["id"]) for item in questions]
first_ids = {
str(item["id"])
for item in json.loads(
(first_20 / "results" / "selected_questions.json").read_text(
encoding="utf-8"
)
)
}
remaining_ids = {
str(item["id"])
for item in json.loads(
(remaining_80 / "results" / "selected_questions.json").read_text(
encoding="utf-8"
)
)
}
if first_ids & remaining_ids:
raise RuntimeError(f"Overlapping IDs: {sorted(first_ids & remaining_ids)}")
if first_ids | remaining_ids != set(ids):
missing = set(ids) - (first_ids | remaining_ids)
extra = (first_ids | remaining_ids) - set(ids)
raise RuntimeError(f"Question mismatch missing={sorted(missing)} extra={sorted(extra)}")
if output.exists() and not args.allow_existing_output:
raise FileExistsError(
f"Refusing to write into existing output directory: {output}. "
"Choose a new --output-dir or pass --allow-existing-output."
)
(output / "results").mkdir(parents=True, exist_ok=args.allow_existing_output)
(output / "analysis").mkdir(parents=True, exist_ok=args.allow_existing_output)
(output / "judge").mkdir(parents=True, exist_ok=args.allow_existing_output)
(output / "results" / "selected_questions.json").write_text(
json.dumps(questions, ensure_ascii=False, indent=2),
encoding="utf-8",
)
counts: dict[str, int] = {}
for config in CONFIGS:
target_dir = output / "results" / config
target_dir.mkdir(parents=True, exist_ok=args.allow_existing_output)
copied = 0
for qid in ids:
source_root = first_20 if qid in first_ids else remaining_80
source = source_root / "results" / config / f"{qid}.json"
if not source.exists():
raise FileNotFoundError(source)
record = json.loads(source.read_text(encoding="utf-8"))
if record.get("error") or not record.get("response"):
raise RuntimeError(f"Invalid result: {source}")
shutil.copy2(source, target_dir / source.name)
copied += 1
counts[config] = copied
manifest = {
"questions": len(ids),
"configs": CONFIGS,
"expected_results": len(ids) * len(CONFIGS),
"copied_results": sum(counts.values()),
"per_config": counts,
"sources": {
"first_20": str(first_20),
"remaining_80": str(remaining_80),
"gold_questions": str(questions_path),
},
}
(output / "merge_manifest.json").write_text(
json.dumps(manifest, ensure_ascii=False, indent=2),
encoding="utf-8",
)
print(json.dumps(manifest, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()
+37
View File
@@ -0,0 +1,37 @@
#!/usr/bin/env python3
from __future__ import annotations
import os
import sys
from pathlib import Path
from fastapi import FastAPI
PROJECT_ROOT = Path(
os.environ.get("GENE_FAMILY_ROOT", "/data/gene-family-mas-v2")
)
GFANALYSIS_ROOT = Path(
os.environ.get("GFANALYSIS_ROOT", "/data/wutx/00test/gfAnalysis")
)
for root in (PROJECT_ROOT, GFANALYSIS_ROOT):
value = str(root)
if value not in sys.path:
sys.path.insert(0, value)
from src.rag.qa_v1_0_0.api import router
app = FastAPI(
title="Plant Biology QA V1.0.0",
version="1.0.0",
description=(
"Provisional-answer-guided, evidence-verified dual-KB QA. "
"This service is independent from the legacy combined QA servers."
),
)
app.include_router(router)
@app.get("/health")
def health():
return {"status": "ok", "service": "qa_v1_0_0", "version": "1.0.0"}
+191
View File
@@ -0,0 +1,191 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import json
import time
from pathlib import Path
from typing import Any, Dict, Iterable, List
import requests
VARIANTS = {
"v1_no_draft_one_round": {
"use_provisional_draft": False,
"max_rounds": 1,
},
"v1_draft_one_round": {
"use_provisional_draft": True,
"max_rounds": 1,
},
"v1_draft_two_rounds": {
"use_provisional_draft": True,
"max_rounds": 2,
},
}
def load_questions(path: Path) -> List[Dict[str, Any]]:
payload = json.loads(path.read_text(encoding="utf-8"))
if isinstance(payload, dict):
for key in ("questions", "items", "data"):
if isinstance(payload.get(key), list):
payload = payload[key]
break
if not isinstance(payload, list):
raise ValueError("Question JSON must be a list or contain questions/items/data")
output = []
for index, item in enumerate(payload, 1):
if isinstance(item, str):
output.append({"question_id": f"Q{index:03d}", "question": item})
continue
if not isinstance(item, dict):
continue
question = str(
item.get("question")
or item.get("query")
or item.get("text")
or ""
).strip()
if question:
output.append(
{
**item,
"question_id": str(
item.get("question_id")
or item.get("id")
or f"Q{index:03d}"
),
"question": question,
}
)
return output
def run_one(
endpoint: str,
question: Dict[str, Any],
variant_id: str,
timeout: float,
) -> Dict[str, Any]:
config = VARIANTS[variant_id]
started = time.perf_counter()
try:
response = requests.post(
endpoint,
json={
"question": question["question"],
"debug": False,
**config,
},
timeout=timeout,
)
payload = response.json()
return {
"question_id": question["question_id"],
"question": question["question"],
"variant_id": variant_id,
"http_status": response.status_code,
"success": bool(payload.get("success")) and response.status_code == 200,
"answer": payload.get("answer", ""),
"source_count": payload.get("source_count", 0),
"round_count": len(payload.get("round_history", []) or []),
"audit_path": payload.get("audit_path", ""),
"timing": payload.get("timing", {}),
"retrieval_hypothesis": payload.get("retrieval_hypothesis", {}),
"claim_verification": payload.get(
"provisional_claim_verification", []
),
"evidence_assessment": payload.get("evidence_assessment", {}),
"sources": payload.get("sources", []),
"error": payload.get("detail", ""),
"client_elapsed": time.perf_counter() - started,
}
except Exception as exc:
return {
"question_id": question["question_id"],
"question": question["question"],
"variant_id": variant_id,
"success": False,
"error": f"{type(exc).__name__}: {exc}",
"client_elapsed": time.perf_counter() - started,
}
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--questions", type=Path, required=True)
parser.add_argument(
"--endpoint",
default="http://127.0.0.1:7909/api/v1/qa-v1/chat",
)
parser.add_argument("--output-dir", type=Path, required=True)
parser.add_argument(
"--variants",
nargs="+",
choices=list(VARIANTS),
default=list(VARIANTS),
)
parser.add_argument("--limit", type=int, default=0)
parser.add_argument("--timeout", type=float, default=900)
args = parser.parse_args()
questions = load_questions(args.questions)
if args.limit > 0:
questions = questions[: args.limit]
args.output_dir.mkdir(parents=True, exist_ok=True)
summary = {
"endpoint": args.endpoint,
"question_file": str(args.questions),
"question_count": len(questions),
"variants": args.variants,
"results": {},
}
for variant_id in args.variants:
variant_dir = args.output_dir / variant_id
variant_dir.mkdir(parents=True, exist_ok=True)
successes = 0
failures = 0
for index, question in enumerate(questions, 1):
target = variant_dir / f"{question['question_id']}.json"
if target.exists():
try:
existing = json.loads(target.read_text(encoding="utf-8"))
if existing.get("success"):
successes += 1
continue
except Exception:
pass
result = run_one(
args.endpoint,
question,
variant_id,
args.timeout,
)
target.write_text(
json.dumps(result, ensure_ascii=False, indent=2),
encoding="utf-8",
)
if result.get("success"):
successes += 1
else:
failures += 1
print(
f"[{variant_id}] {index}/{len(questions)} "
f"{question['question_id']} success={result.get('success')}",
flush=True,
)
summary["results"][variant_id] = {
"successes": successes,
"failures": failures,
}
(args.output_dir / "run_summary.json").write_text(
json.dumps(summary, ensure_ascii=False, indent=2),
encoding="utf-8",
)
if __name__ == "__main__":
main()
+67
View File
@@ -0,0 +1,67 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[1]
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
from src.rag.extractors.v16_1_2.v16_extractor import V16Extractor
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Run the formal V16.1.2 plant-science SRT extraction pipeline."
)
parser.add_argument("--pdf", required=True, type=Path)
parser.add_argument("--output-dir", required=True, type=Path)
parser.add_argument("--pmid", default="")
parser.add_argument("--doi", default="")
parser.add_argument("--disable-llm", action="store_true")
parser.add_argument("--use-pc-data", action="store_true")
return parser.parse_args()
def main() -> int:
args = parse_args()
pdf = args.pdf.resolve()
output_dir = args.output_dir.resolve()
if not pdf.exists():
raise SystemExit(f"PDF does not exist: {pdf}")
output_dir.mkdir(parents=True, exist_ok=True)
extractor = V16Extractor(
output_dir=output_dir,
use_mineru=True,
use_llm_relations=not args.disable_llm,
use_rule_relation_fallback=False,
use_vlm=False,
use_neo4j=False,
use_unified_storage=False,
use_llm_definitions=False,
use_pc_data=args.use_pc_data,
use_disambiguation=args.use_pc_data,
)
result = extractor.extract(
pdf,
pmid=args.pmid or None,
doi=args.doi or None,
)
summary = {
"version": result.get("metadata", {}).get("version"),
"pdf": str(pdf),
"output_dir": str(output_dir),
"entities": len(result.get("entities", [])),
"relations": len(result.get("relations", [])),
"relation_units": len(result.get("chunks", [])),
"result_json": str(output_dir / "result.json"),
}
print(json.dumps(summary, ensure_ascii=False, indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main())
+185
View File
@@ -0,0 +1,185 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import concurrent.futures
import json
import os
import subprocess
import sys
import time
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[1]
def build_run_command(
row: dict,
root: Path,
output_dir: Path,
python: Path,
*,
use_pc_data: bool = False,
disable_llm: bool = False,
) -> list[str]:
command = [
str(python),
str(root / "scripts/run_v16_1_2_one_paper.py"),
"--pdf",
str(Path(row["destination_path"])),
"--output-dir",
str(output_dir),
]
pmid = str(row.get("pmid") or "").strip()
doi = str(row.get("doi") or "").strip()
if pmid:
command.extend(["--pmid", pmid])
if doi:
command.extend(["--doi", doi])
if use_pc_data:
command.append("--use-pc-data")
if disable_llm:
command.append("--disable-llm")
return command
def run_one(
row: dict,
root: Path,
output_root: Path,
python: Path,
*,
use_pc_data: bool = False,
disable_llm: bool = False,
) -> dict:
pdf = Path(row["destination_path"])
paper_id = str(row.get("paper_id") or pdf.stem)
out = output_root / paper_id
out.mkdir(parents=True, exist_ok=True)
command = [
*build_run_command(
row,
root,
out,
python,
use_pc_data=use_pc_data,
disable_llm=disable_llm,
)
]
log_path = out / "run.log"
started = time.time()
with log_path.open("w", encoding="utf-8") as log:
process = subprocess.run(
command,
cwd=root,
env=os.environ.copy(),
stdout=log,
stderr=subprocess.STDOUT,
text=True,
)
ended = time.time()
result_path = out / "result.json"
result = {
**row,
"paper_id": paper_id,
"output_dir": str(out),
"result_json": str(result_path),
"return_code": process.returncode,
"wall_seconds": round(ended - started, 3),
"success": process.returncode == 0 and result_path.exists(),
}
if result["success"]:
command_text = " ".join(
f"'{arg}'" if " " in arg else arg for arg in command
)
(out / "SUCCESSFUL_COMMAND.md").write_text(
"# Successful V16.1.2 command\n\n"
"Environment: current process environment/private `.env`.\n\n"
f"```bash\n{command_text}\n```\n",
encoding="utf-8",
)
return result
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--manifest", required=True, type=Path)
parser.add_argument("--output-root", required=True, type=Path)
parser.add_argument("--workers", type=int, default=3)
parser.add_argument("--use-pc-data", action="store_true")
parser.add_argument("--disable-llm", action="store_true")
parser.add_argument(
"--project-root",
type=Path,
default=Path(os.environ.get("PLANTAI_PROJECT_ROOT", PROJECT_ROOT)),
)
parser.add_argument(
"--python",
type=Path,
default=Path(os.environ.get("PLANTAI_PYTHON", sys.executable)),
)
args = parser.parse_args()
if args.workers < 1:
raise SystemExit("--workers must be >= 1")
rows = json.loads(args.manifest.read_text(encoding="utf-8"))
args.output_root.mkdir(parents=True, exist_ok=True)
started = time.time()
results: list[dict] = []
with concurrent.futures.ThreadPoolExecutor(max_workers=args.workers) as pool:
future_map = {
pool.submit(
run_one,
row,
args.project_root,
args.output_root,
args.python,
use_pc_data=args.use_pc_data,
disable_llm=args.disable_llm,
): row
for row in rows
}
for future in concurrent.futures.as_completed(future_map):
row = future_map[future]
try:
result = future.result()
except Exception as exc:
result = {
**row,
"paper_id": Path(row["file_name"]).stem,
"return_code": -1,
"wall_seconds": 0,
"success": False,
"error": repr(exc),
}
results.append(result)
results.sort(key=lambda item: int(item["index"]))
(args.output_root / "batch_progress.json").write_text(
json.dumps(results, ensure_ascii=False, indent=2),
encoding="utf-8",
)
print(
f"[{len(results)}/{len(rows)}] "
f"{result['paper_id']} success={result['success']} "
f"wall={result['wall_seconds']}s",
flush=True,
)
summary = {
"started_epoch": started,
"finished_epoch": time.time(),
"wall_seconds": round(time.time() - started, 3),
"workers": args.workers,
"total": len(results),
"success": sum(bool(item["success"]) for item in results),
"failed": sum(not bool(item["success"]) for item in results),
"results": results,
}
(args.output_root / "BATCH_RUN_SUMMARY.json").write_text(
json.dumps(summary, ensure_ascii=False, indent=2),
encoding="utf-8",
)
return 0 if summary["failed"] == 0 else 1
if __name__ == "__main__":
raise SystemExit(main())
+43
View File
@@ -0,0 +1,43 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT=/data/gene-family-mas-v2
GFANALYSIS_ROOT=/data/wutx/00test/gfAnalysis
if [[ -f "$ROOT/.env" ]]; then
set -a
source "$ROOT/.env"
set +a
fi
if [[ -f "$ROOT/.env.v15_deepseek" ]]; then
set -a
source "$ROOT/.env.v15_deepseek"
set +a
fi
if [[ -f "$ROOT/.runtime/taxonomy_bge_7907/service.env" ]]; then
set -a
source "$ROOT/.runtime/taxonomy_bge_7907/service.env"
set +a
fi
export GENE_FAMILY_ROOT="$ROOT"
export GFANALYSIS_ROOT
export PYTHONPATH="$ROOT:$GFANALYSIS_ROOT"
export V15_QDRANT_COLLECTION="${V15_QDRANT_COLLECTION:-v15_q1q2_1770_srt_bge_20260610}"
export PC_QDRANT_COLLECTION="${PC_QDRANT_COLLECTION:-pc_srt_bge_full_20260620}"
export BGE_RERANKER_MODEL="${BGE_RERANKER_MODEL:-$ROOT/bge-reranker-base}"
export QA_V1_ENABLE_PROVISIONAL_DRAFT="${QA_V1_ENABLE_PROVISIONAL_DRAFT:-1}"
export QA_V1_ENABLE_SECOND_ROUND="${QA_V1_ENABLE_SECOND_ROUND:-1}"
export QA_V1_ENABLE_BGE_RERANK="${QA_V1_ENABLE_BGE_RERANK:-1}"
export QA_V1_BGE_DEVICE="${QA_V1_BGE_DEVICE:-cuda}"
export QA_V1_MAX_ROUNDS="${QA_V1_MAX_ROUNDS:-2}"
cd "$ROOT"
exec /data/miniconda3/envs/gfpy312/bin/python -m uvicorn \
scripts.qa_v1_0_0_server:app \
--host 0.0.0.0 \
--port "${QA_V1_PORT:-7908}"
+36
View File
@@ -0,0 +1,36 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import json
import os
import sys
from pathlib import Path
ROOT = Path(os.environ.get("GENE_FAMILY_ROOT", "/data/gene-family-mas-v2"))
GF = Path(os.environ.get("GFANALYSIS_ROOT", "/data/wutx/00test/gfAnalysis"))
for path in (ROOT, GF):
if str(path) not in sys.path:
sys.path.insert(0, str(path))
from src.rag.qa_v1_0_0.pipeline import QAV1Pipeline
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("question")
parser.add_argument("--no-draft", action="store_true")
parser.add_argument("--max-rounds", type=int, default=2)
parser.add_argument("--debug", action="store_true")
args = parser.parse_args()
response = QAV1Pipeline().answer(
args.question,
debug=args.debug,
use_provisional_draft=not args.no_draft,
max_rounds=args.max_rounds,
)
print(json.dumps(response, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()