The product could only reach a GPU through one hard-coded route: `wsl.exe -d Ubuntu`, a device named "RTX 4090", and launch scripts that existed only as PowerShell. On this machine `WSL_DISTRO_NAME` is already set, so the local shell is Linux and the wsl.exe hop was unnecessary; on any other Linux host it is impossible. Every SGLang, vLLM and KTransformers profile was reported "missing" here even with a working 4090. profiles.py now asks the host what it is instead of assuming: `uses_wsl()` is false inside WSL and only true when wsl.exe is actually on PATH, `host_shell()` runs `bash -lc` directly or through `wsl.exe -d settings.wsl_distro`, `gpu_available()` replaced `wsl_gpu_available()`, and `launch_script()` picks the .sh twin on a Linux host and the .ps1 on a Windows one. The schema field `wsl_gpu` became `gpu_available` because the old name lied on Linux. Training no longer fails on a card name. `cuda_bf16_training()` gates on cuda_available plus bf16_supported, which the training environment already records, so an RTX 5090 or a 4080 passes and a non-BF16 4090 still fails. Fourteen .sh twins cover the six launch profiles plus install, verify, serve, download and finetune; the six WSL-hopping .ps1 files read ELY_EYE_WSL_DISTRO instead of Ubuntu. PowerShell parsing is not verified here because no pwsh exists on this host. ci.yml runs pytest, ruff, go build/vet/test/gofmt, the Vite build and a docker build-plus-curl on every push. To make that lint gate mean anything, the 17 pre-existing ruff findings across backend/src are cleared: 14 mechanical fixes plus three probe handlers that now name the failure families they absorb (OSError, SubprocessError, ValueError) instead of catching Exception. Verification: pytest 239 passed; ruff check backend/src bench clean (17 at HEAD); mypy on the 12 touched files 14 errors, identical at HEAD; pyright on the same files 8 errors, identical at HEAD; bash -n clean on all 15 .sh.
69 lines
2.5 KiB
Python
69 lines
2.5 KiB
Python
from __future__ import annotations
|
|
|
|
import hashlib
|
|
|
|
from .config import Settings, get_settings
|
|
from .db import Database
|
|
from .schemas import CitationReport, CompiledContext
|
|
|
|
|
|
class EvidenceGraphService:
|
|
def __init__(self, settings: Settings | None = None, database: Database | None = None) -> None:
|
|
self.settings = settings or get_settings()
|
|
self.db = database or Database(self.settings)
|
|
|
|
def record_answer(
|
|
self,
|
|
question: str,
|
|
answer: str,
|
|
report: CitationReport,
|
|
context: CompiledContext,
|
|
) -> str:
|
|
claim_id = claim_node_id(question, answer)
|
|
hit_by_id = {hit.atom.atom_id: hit for hit in context.hits}
|
|
edges: list[dict[str, object]] = []
|
|
for atom_id in report.cited_atom_ids:
|
|
hit = hit_by_id.get(atom_id)
|
|
edges.append(
|
|
{
|
|
"from_atom_id": claim_id,
|
|
"to_atom_id": atom_id,
|
|
"relation": "cites",
|
|
"confidence": report.confidence,
|
|
"metadata": {
|
|
"question": question,
|
|
"answer_sha256": hashlib.sha256(answer.encode("utf-8")).hexdigest(),
|
|
"cache_trace_id": context.cache_trace_id,
|
|
"final_score": hit.final_score if hit else None,
|
|
},
|
|
}
|
|
)
|
|
for atom_id in report.missing_atom_ids:
|
|
edges.append(
|
|
{
|
|
"from_atom_id": claim_id,
|
|
"to_atom_id": atom_id,
|
|
"relation": "missing_citation",
|
|
"confidence": 0.0,
|
|
"metadata": {"question": question, "cache_trace_id": context.cache_trace_id},
|
|
}
|
|
)
|
|
for note in report.contradiction_notes:
|
|
note_id = "contradiction_" + hashlib.sha256(note.encode("utf-8")).hexdigest()[:16]
|
|
edges.append(
|
|
{
|
|
"from_atom_id": claim_id,
|
|
"to_atom_id": note_id,
|
|
"relation": "contradiction_note",
|
|
"confidence": report.confidence,
|
|
"metadata": {"note": note, "question": question},
|
|
}
|
|
)
|
|
self.db.write_evidence_edges(edges)
|
|
return claim_id
|
|
|
|
|
|
def claim_node_id(question: str, answer: str) -> str:
|
|
digest = hashlib.sha256(f"{question}\n{answer}".encode()).hexdigest()[:20]
|
|
return f"claim_{digest}"
|