| """ |
| ByteAstra — Evaluation & Grounding QA Script. |
| |
| Runs a set of predefined test queries against the tutoring engine and |
| reports on: |
| - Whether grounded answers are returned for in-syllabus questions |
| - Whether out-of-syllabus responses are returned for off-topic questions |
| - Whether domain isolation is preserved (cross-domain queries should not leak) |
| - Citation validity |
| |
| Usage: |
| python scripts/evaluate.py --domain ayurveda |
| |
| Add more test cases in the TESTS list below as the knowledge base grows. |
| """ |
| from __future__ import annotations |
| import asyncio |
| import json |
| import logging |
| import sys |
| from dataclasses import dataclass, field |
| from pathlib import Path |
| from typing import Literal |
|
|
| sys.path.insert(0, str(Path(__file__).parent.parent)) |
| sys.stdout.reconfigure(encoding='utf-8') |
|
|
| from app.services import engine |
|
|
| logging.basicConfig(level=logging.WARNING, format="%(levelname)s | %(message)s") |
| logger = logging.getLogger(__name__) |
|
|
|
|
| |
|
|
| @dataclass |
| class TestCase: |
| id: str |
| domain: str |
| query: str |
| expected_grounded: bool |
| description: str |
| history: list[dict] = field(default_factory=list) |
|
|
|
|
| |
|
|
| TEST_CASES: list[TestCase] = [ |
| |
| TestCase( |
| id="ayurveda_001", |
| domain="ayurveda", |
| query="What are the three Doshas in Ayurveda and their qualities?", |
| expected_grounded=True, |
| description="Core Tridosha concept — should always be in the KB.", |
| ), |
| TestCase( |
| id="ayurveda_002", |
| domain="ayurveda", |
| query="Explain the concept of Agni in Ayurveda and its role in digestion.", |
| expected_grounded=True, |
| description="Agni concept — fundamental to BAMS syllabus.", |
| ), |
| TestCase( |
| id="ayurveda_003", |
| domain="ayurveda", |
| query="What are the seven Dhatus according to Ayurvedic texts?", |
| expected_grounded=True, |
| description="Sapta Dhatu — core anatomy concept.", |
| ), |
| |
| TestCase( |
| id="oos_001", |
| domain="ayurveda", |
| query="What is the mechanism of action of ibuprofen?", |
| expected_grounded=False, |
| description="Allopathic pharmacology — should NOT be in Ayurveda KB.", |
| ), |
| TestCase( |
| id="oos_002", |
| domain="ayurveda", |
| query="Explain Section 420 of the Indian Penal Code.", |
| expected_grounded=False, |
| description="Law question — completely off-domain.", |
| ), |
| |
| TestCase( |
| id="isolation_001", |
| domain="ayurveda", |
| query="What are the anatomical layers of the skin according to modern anatomy?", |
| expected_grounded=False, |
| description="MBBS-style question posed to Ayurveda domain — should not bleed.", |
| ), |
| ] |
|
|
|
|
| |
|
|
| @dataclass |
| class TestResult: |
| case: TestCase |
| actual_grounded: bool |
| citations_count: int |
| answer_preview: str |
| passed: bool |
| failure_reason: str = "" |
|
|
|
|
| async def run_test(case: TestCase) -> TestResult: |
| full_content: list[str] = [] |
| actual_grounded = False |
| citations_count = 0 |
|
|
| try: |
| async for event in engine.stream_answer(case.domain, case.query, case.history): |
| if event["type"] == "citations": |
| actual_grounded = event.get("is_grounded", False) |
| citations_count = len(event.get("citations", [])) |
| elif event["type"] == "delta": |
| full_content.append(event.get("content", "")) |
| elif event["type"] == "error": |
| return TestResult( |
| case=case, |
| actual_grounded=False, |
| citations_count=0, |
| answer_preview="ERROR: " + event.get("error", "unknown"), |
| passed=False, |
| failure_reason="Engine returned error", |
| ) |
| except Exception as e: |
| return TestResult( |
| case=case, |
| actual_grounded=False, |
| citations_count=0, |
| answer_preview="", |
| passed=False, |
| failure_reason=f"Exception: {e}", |
| ) |
|
|
| answer = "".join(full_content).strip() |
| grounded_match = actual_grounded == case.expected_grounded |
| passed = grounded_match |
|
|
| failure_reason = "" |
| if not grounded_match: |
| if case.expected_grounded: |
| failure_reason = f"Expected grounded answer but got out-of-syllabus response." |
| else: |
| failure_reason = f"Expected out-of-syllabus response but got grounded answer with {citations_count} citation(s)." |
|
|
| return TestResult( |
| case=case, |
| actual_grounded=actual_grounded, |
| citations_count=citations_count, |
| answer_preview=answer[:200], |
| passed=passed, |
| failure_reason=failure_reason, |
| ) |
|
|
|
|
| async def run_suite(domain_filter: str | None = None) -> None: |
| cases = TEST_CASES |
| if domain_filter: |
| cases = [c for c in cases if c.domain == domain_filter] |
|
|
| print(f"\n{'='*65}") |
| print(f" ByteAstra Evaluation Suite — {len(cases)} test(s)") |
| print(f"{'='*65}\n") |
|
|
| results: list[TestResult] = [] |
| for case in cases: |
| print(f" ▶ [{case.id}] {case.description}") |
| result = await run_test(case) |
| results.append(result) |
| status = "✓ PASS" if result.passed else "✗ FAIL" |
| grounded_label = "grounded" if result.actual_grounded else "out-of-syllabus" |
| print(f" {status} | {grounded_label} | {result.citations_count} citation(s)") |
| if not result.passed: |
| print(f" ⚠ {result.failure_reason}") |
| if result.answer_preview: |
| print(f" Preview: {result.answer_preview[:120]}...") |
| print() |
|
|
| passed = sum(1 for r in results if r.passed) |
| failed = len(results) - passed |
| print(f"{'='*65}") |
| print(f" Results: {passed}/{len(results)} passed | {failed} failed") |
| print(f"{'='*65}\n") |
|
|
| if failed: |
| sys.exit(1) |
|
|
|
|
| if __name__ == "__main__": |
| import argparse |
| parser = argparse.ArgumentParser() |
| parser.add_argument("--domain", default=None, help="Filter tests to a specific domain") |
| args = parser.parse_args() |
| asyncio.run(run_suite(domain_filter=args.domain)) |
|
|