File size: 5,008 Bytes
48ee375
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
#!/usr/bin/env python3
"""Knowledge-retrieval smoke test for the local RAG index.

Runs a fixed set of canonical control-engineering queries against the live
BM25 index used by the agent (controlai_rag.index.ControlRAGIndex) and checks
that each query's top hits actually contain at least one expected keyword.
Also reports whether each query's best score clears the 2.5 relevance
threshold that ControlAIAgent._get_grounded_instruction uses to decide
whether to inject retrieved text into the system prompt -- a query can
retrieve "correct" chunks yet still never get grounded into an answer if its
score sits below that bar.

Usage:
    python3 scripts/test_rag_knowledge.py
"""

from __future__ import annotations

import sys
from pathlib import Path

PROJECT_ROOT = Path(__file__).resolve().parent.parent
if str(PROJECT_ROOT) not in sys.path:
    sys.path.insert(0, str(PROJECT_ROOT))

from controlai_rag.index import ControlRAGIndex

GROUNDING_SCORE_THRESHOLD = 2.5

# (query, keywords where at least one must appear in a top-k hit's text)
# ControlAI is a general control-engineering agent, so this suite deliberately
# spans every application domain -- aerospace, automotive, robotics, industrial
# automation, power -- not just classical/modern theory.
TEST_CASES: list[tuple[str, list[str]]] = [
    # --- Core theory ---
    ("controllability matrix rank test", ["controllab", "rank"]),
    ("observability of linear time invariant systems", ["observ"]),
    ("continuous algebraic Riccati equation LQR", ["riccati", "lqr", "quadratic"]),
    ("discrete algebraic Riccati equation DARE", ["riccati", "discrete"]),
    ("zero order hold ZOH discretization", ["zero-order", "zero order", "hold", "discret"]),
    ("Lyapunov stability of nonlinear systems", ["lyapunov", "stab"]),
    ("gain margin phase margin frequency response", ["gain margin", "phase margin"]),
    ("Kalman filter state estimation", ["kalman", "estimat"]),
    ("PID controller tuning", ["pid", "proportional"]),
    ("root locus method", ["root locus"]),
    ("PBH test for controllability", ["pbh", "popov"]),
    ("model predictive control constrained optimization", ["model predictive", "mpc", "horizon"]),
    ("H-infinity robust control small gain theorem", ["h-infinity", "h infinity", "small gain", "hinf"]),
    ("control barrier function safety filter", ["barrier", "safety"]),
    ("state feedback pole placement", ["pole placement", "state feedback"]),
    # --- Application domains ---
    ("aircraft flight control longitudinal dynamics", ["aircraft", "flight", "longitudinal", "pitch"]),
    ("quadrotor UAV attitude control", ["quadrotor", "uav", "attitude", "drone"]),
    ("vehicle dynamics yaw rate stability control", ["vehicle", "yaw", "tire", "steering"]),
    ("automotive cruise control design", ["cruise", "vehicle", "throttle", "speed"]),
    ("robot manipulator kinematics and Jacobian", ["manipulator", "jacobian", "kinematic", "robot"]),
    ("mobile robot localization and odometry", ["odometry", "localiz", "mobile robot", "slam"]),
    ("industrial process control valve saturation", ["valve", "process", "saturat", "actuator"]),
    ("cascade control loop in process automation", ["cascade", "process", "inner loop", "secondary"]),
    ("electric motor drive speed control", ["motor", "drive", "torque", "induction"]),
    ("system identification from input output data", ["identification", "arx", "least squares", "estimat"]),
]


def run() -> int:
    index = ControlRAGIndex()
    if not index.chunks or not index.bm25:
        print("FAIL: RAG index did not load (no chunks / no BM25 model). Is data/rag_index/ populated?")
        return 1

    print(f"RAG index loaded: {len(index.chunks)} chunks\n")

    passed = 0
    grounded = 0
    for query, keywords in TEST_CASES:
        hits = index.search(query, top_k=5)
        best_score = hits[0]["score"] if hits else 0.0
        matched = any(
            kw.lower() in hit["text"].lower()
            for hit in hits
            for kw in keywords
        )
        would_ground = best_score > GROUNDING_SCORE_THRESHOLD
        grounded += int(would_ground)
        passed += int(matched)

        status = "PASS" if matched else "FAIL"
        ground_tag = "grounds" if would_ground else "below threshold"
        print(f"[{status}] '{query}'  best_score={best_score:.2f} ({ground_tag})")
        if hits:
            top = hits[0]
            excerpt = " ".join(top["text"].split())[:160]
            print(f"        top hit: [{top['filename']} p.{top['page']}] {excerpt}...")
        else:
            print("        no hits returned")
        print()

    total = len(TEST_CASES)
    print("=" * 70)
    print(f"Keyword relevance: {passed}/{total} queries retrieved an on-topic chunk")
    print(f"Grounding trigger:  {grounded}/{total} queries would clear the score>{GROUNDING_SCORE_THRESHOLD} auto-grounding bar")
    print("=" * 70)

    return 0 if passed == total else 1


if __name__ == "__main__":
    raise SystemExit(run())