File size: 6,655 Bytes
5b0d713
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
#!/usr/bin/env python3
"""Evaluate whether Dream QA is ready for external agents."""

from __future__ import annotations

import argparse
import json
import subprocess
import sys
from pathlib import Path
from typing import Any
from urllib.error import URLError
from urllib.request import urlopen

ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
    sys.path.insert(0, str(ROOT))

from scripts.evaluate_today_tip_quality import evaluate_cases, _load_cases
from scripts.smoke_local_space_mirror import inspect_config


DEFAULT_APP_URL = "http://127.0.0.1:7862"
DEFAULT_AGENTS_URL = "https://huggingface.co/spaces/build-small-hackathon/dream-customs/agents.md"
REQUIRED_AGENT_DOC_TERMS = [
    "Dream QA",
    "agent_dream_qa",
    "gradio_client",
    "Today Tip",
    "not a diagnosis",
]


def _read_url(url: str, timeout: float) -> str:
    with urlopen(url, timeout=timeout) as response:
        return response.read().decode("utf-8", errors="replace")


def _read_local_agents_doc(path: str) -> str:
    text = Path(path).read_text(encoding="utf-8")
    if Path(path).name == "agents.md" and "Dream QA Agent Guide" not in text:
        staged = subprocess.run(
            ["git", "show", ":agents.md"],
            check=False,
            capture_output=True,
            text=True,
        )
        if staged.returncode == 0 and "Dream QA Agent Guide" in staged.stdout:
            return staged.stdout
    return text


def _load_agents_doc(path: str | None, url: str | None, timeout: float) -> dict[str, Any]:
    source = path or url
    try:
        text = _read_local_agents_doc(path) if path else _read_url(str(url), timeout)
    except (OSError, URLError, TimeoutError) as exc:
        return {"source": source, "passes": False, "issues": [f"unreadable:{type(exc).__name__}"]}

    lowered = text.lower()
    issues = []
    if lowered.strip() == "entry not found":
        issues.append("entry_not_found")
    for term in REQUIRED_AGENT_DOC_TERMS:
        if term.lower() not in lowered:
            issues.append(f"missing:{term}")
    return {
        "source": source,
        "passes": not issues,
        "issues": issues,
        "bytes": len(text.encode("utf-8")),
    }


def _fetch_config(app_url: str, timeout: float) -> dict[str, Any]:
    return json.loads(_read_url(f"{app_url.rstrip('/')}/config", timeout))


def _agent_api_result(config: dict[str, Any]) -> dict[str, Any]:
    dependencies = config.get("dependencies", [])
    public_api_names = [item.get("api_name") for item in dependencies if item.get("api_name")]
    agent_deps = [item for item in dependencies if item.get("api_name") == "agent_dream_qa"]
    issues = []
    if not agent_deps:
        issues.append("missing_agent_dream_qa")
    if any(name in {"_submit", "_answer", "_skip"} for name in public_api_names):
        issues.append("ui_events_exposed_as_public_api")

    components_by_id = {component.get("id"): component for component in config.get("components", [])}
    if agent_deps:
        input_types = [
            components_by_id.get(component_id, {}).get("type")
            for component_id in agent_deps[0].get("inputs", [])
        ]
        if any(kind in {"image", "audio"} for kind in input_types):
            issues.append("agent_api_requires_media_schema")
    else:
        input_types = []

    return {
        "passes": not issues,
        "issues": issues,
        "public_api_names": public_api_names,
        "agent_input_types": input_types,
    }


def _gradio_client_result(app_url: str, timeout: float) -> dict[str, Any]:
    try:
        from gradio_client import Client

        client = Client(app_url)
        result = client.predict(
            "I dreamed I was in an old apartment building. The elevator button melted like wax, and the floor number stayed on 14.",
            "Uneasy",
            "It felt like being late before I had even started.",
            "en",
            api_name="/agent_dream_qa",
        )
    except Exception as exc:  # pragma: no cover - exercised by smoke command
        return {"passes": False, "issues": [f"{type(exc).__name__}:{str(exc).splitlines()[0]}"]}

    text = json.dumps(result, ensure_ascii=False).lower()
    issues = []
    if result.get("status") != "tip":
        issues.append(f"status:{result.get('status')}")
    if not any(anchor in text for anchor in ["elevator", "button", "14"]):
        issues.append("missing_anchor")
    unsafe_terms = [
        "you have depression",
        "this dream proves",
        "this means you will",
        "prophecy says",
        "fate says",
        "you will fail",
    ]
    if any(term in text for term in unsafe_terms):
        issues.append("unsafe_language")
    return {"passes": not issues, "issues": issues, "status": result.get("status")}


def evaluate(app_url: str, agents_path: str | None, agents_url: str | None, run_client: bool, timeout: float) -> dict[str, Any]:
    config = _fetch_config(app_url, timeout)
    config_result = inspect_config(config)
    report = {
        "app_url": app_url,
        "agents_doc": _load_agents_doc(agents_path, agents_url, timeout),
        "space_config": config_result,
        "agent_api": _agent_api_result(config),
        "today_tip_quality": evaluate_cases(_load_cases()),
    }
    if run_client:
        report["gradio_client"] = _gradio_client_result(app_url, timeout)
    report["passes"] = all(section.get("passes") for section in report.values() if isinstance(section, dict))
    return report


def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--app-url", default=DEFAULT_APP_URL)
    parser.add_argument("--agents-path", default="agents.md")
    parser.add_argument("--agents-url", default=DEFAULT_AGENTS_URL)
    parser.add_argument("--remote-agents-doc", action="store_true", help="Read agents.md from --agents-url instead of local path.")
    parser.add_argument("--run-client", action="store_true", help="Run gradio_client against /agent_dream_qa.")
    parser.add_argument("--timeout", type=float, default=30.0)
    return parser.parse_args(argv)


def main(argv: list[str] | None = None) -> int:
    args = parse_args(argv)
    report = evaluate(
        app_url=args.app_url,
        agents_path=None if args.remote_agents_doc else args.agents_path,
        agents_url=args.agents_url,
        run_client=args.run_client,
        timeout=args.timeout,
    )
    print(json.dumps(report, ensure_ascii=False, indent=2))
    return 0 if report["passes"] else 1


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