Key Commands
# Install (editable, includes ML deps)
pip install -e .
# Run server locally (port 7860)
uvicorn server.app:app --host 0.0.0.0 --port 7860 --reload
# Run heuristic baseline (no API key needed)
python inference.py --heuristic --env-url http://localhost:7860
# Run with LLM (Groq/Ollama/OpenAI-compatible)
export API_BASE_URL=https://api.groq.com/openai/v1
export MODEL_NAME=llama-3.3-70b-versatile
export HF_TOKEN=your_key
python inference.py --env-url http://localhost:7860 --episodes 3 --steps 5
# Run tests
pytest tests/ -v
pytest tests/test_grader.py::TestRewardRange -v # Specific class
pytest tests/test_grader.py::TestRewardRange::test_reward_in_range_correct_answer -v # Single test
# Validate OpenEnv compliance
openenv validate # Local structure check
openenv validate --url http://localhost:7860 # Runtime check against live server
# Lint
ruff check . --ignore E501,F401,F403
# Docker
docker build -t hallucination-guard-env .
docker run -p 7860:7860 hallucination-guard-env
Architecture
OpenEnv RL environment for training LLMs to avoid hallucinations. FastAPI server on HuggingFace Spaces with 3 graded tasks and a 9-component reward system.
Data flow: POST /reset β sample question from dataset β POST /step(answer) β grade via 9-component grader β POST /grader β 0.0β1.0 per episode
Core Modules
| Module | Role |
|---|---|
server/app.py |
FastAPI endpoints, session management, inline Gradio-style HTML docs |
server/environment.py |
HallucinationEnvironment β reset/step/state, curriculum, early stopping |
server/grader.py |
calculate_reward() β 9 components, hallucination detection, refusal handling |
server/dataset_loader.py |
Loads 38 datasets from SamSankar/hallucination-guard-cache HF Dataset repo |
server/tasks.py |
3 TaskDefinition objects + compute_task_score() per-episode grader |
models.py |
Pydantic models inheriting from openenv.core.env_server base classes |
inference.py |
Hackathon submission script with [START]/[STEP]/[END] log format |
Session Isolation
/reset creates a per-session HallucinationEnvironment that shares the global dataset loader (expensive) but has its own episode state. Sessions tracked by episode_id/session_id in _sessions dict. /step looks up the session β must pass session_id or it falls back to a shared default env that may be in stale state. The EnvClient in inference.py stores _session_id from reset and passes it on step.
Reward System (grader.py)
calculate_reward() returns (reward: float, info: dict). 9 components with weights:
| Component | Weight | Implementation |
|---|---|---|
| factual_correctness | 0.35 | check_factual_accuracy_advanced() β exact/fuzzy/semantic match |
| source_grounding | 0.20 | check_quote_in_context_advanced() β reduced for wrong answers |
| citation_accuracy | 0.10 | Verbatim quote match in context |
| confidence_calibration | 0.10 | compute_calibration_error() β overconfidence penalized more |
| semantic_consistency | 0.10 | NLI entailment (DeBERTa-v3 CrossEncoder) |
| hallucination_penalty | 0.10 | detect_hallucination_advanced() β type + severity classification |
| rouge_score | 0.02 | ROUGE-1/2/L |
| bertscore | 0.02 | Uses roberta-base |
| alignscore | 0.01 | NLI CrossEncoder-based alignment (no separate model β reuses _get_nli()) |
Key grader behavior:
- Wrong answers capped at ~0.4 regardless of grounding (
factual_cap) - Refusal on unanswerable questions: rewarded 0.65β0.80
- Refusal when answer exists: penalized to 0.30
- Numerical fabrication checked on original text (before
normalize_textreplaces numbers withNUM) - Thinking traces (Nemotron
βΈ,<reasoning>) stripped via_strip_thinking()before grading
ML Model Loading (lazy singletons)
All ML models load on first use via module-level globals in grader.py:
_get_embedder()βall-MiniLM-L6-v2(sentence-transformers)_get_nli()βcross-encoder/nli-deberta-v3-small(also used bycompute_alignscore)_get_rouge()βrouge_score.rouge_scorer.RougeScorer_check_bertscore()βbert_scorepackage withroberta-base
app.py lifespan preloads these in a background thread so first request isn't slow. Dockerfile pre-caches them at build time.
Task Scoring (tasks.py)
compute_task_score() aggregates per-step rewards into 0.0β1.0:
- Base = mean step reward - hallucination penalty
- Completion bonus (+0.02) for episodes with β₯5 steps
- Task-3 overconfidence penalty:
max(0, avg_calibration - 0.7) * avg_hallucination * 0.1
Dataset Loading
Core datasets (squad, halueval, boolq, openbookqa, sciq) load synchronously at startup. Extended datasets download in a background thread from SamSankar/hallucination-guard-cache on HF Hub. Cache at /tmp/halluguard_cache/.
Pydantic Models
All inherit from openenv.core.env_server (Action, Observation, State). Use Field(default_factory=...) not field(default_factory=...). Use str for enum values in model fields.
Serialization uses _safe_dict() in app.py which handles Pydantic models, dataclasses, and enums recursively.
Log Format (inference.py)
inference.py emits structured logs required by the hackathon evaluator:
[START] task=<task_name> env=hallucination-guard-env model=<model_name>
[STEP] step=<n> action=<action_str> reward=<0.00> done=<true|false> error=<msg|null>
[END] success=<true|false> steps=<n> score=<score> rewards=<r1,r2,...,rn>
Critical Constraints
- NumPy <2.0.0 β Pre-compiled packages crash with NumPy 2.x
- BERTScore model β Must use
roberta-base, NOTmicrosoft/deberta-v3-base(fast tokenizer crashes with transformers>=4.57 due tovocab_file=None) - AlignScore β Replaced with NLI CrossEncoder-based
compute_alignscore()(no separate package/model needed; do NOT addalignscoreto requirements) /metadatamust includedescriptionβ OpenEnv validator fails without it/schemamust includestateβ OpenEnv validator fails without it- Port is 7860 β HF Spaces config, Dockerfile, and client.py all use this
- session_id β
inference.pyandclient.pymust passsession_idfrom/resetto/stepor episodes end after 1 step
Repositories
- GitHub: https://github.com/SS-360/hallucination-guard-env
- HuggingFace Space: https://huggingface.co/spaces/SamSankar/hallucination-guard-env