218 lines
6.2 KiB
Python
Executable File
218 lines
6.2 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
import json
|
|
import os
|
|
import sys
|
|
import urllib.request
|
|
import urllib.error
|
|
from pathlib import Path
|
|
from typing import Any, Dict, List
|
|
|
|
try:
|
|
import psycopg
|
|
except Exception:
|
|
psycopg = None
|
|
|
|
|
|
def env(name: str, default: str = "") -> str:
|
|
return os.getenv(name, default).strip()
|
|
|
|
|
|
API_URL = env("EVAL_API_URL", "http://127.0.0.1:8020/analyze")
|
|
DATASET = Path(env("EVAL_DATASET", "data/action_eval_cases.jsonl"))
|
|
PSQL_DATABASE_URL = env("PSQL_DATABASE_URL")
|
|
EVAL_CLEANUP = env("EVAL_CLEANUP", "true").lower() == "true"
|
|
EVAL_LIMIT = int(env("EVAL_LIMIT", "0") or "0")
|
|
EVAL_LLM_ONLY = env("EVAL_LLM_ONLY", "false").lower() == "true"
|
|
|
|
|
|
def load_cases() -> List[Dict[str, Any]]:
|
|
cases = []
|
|
with DATASET.open("r", encoding="utf-8") as f:
|
|
for line in f:
|
|
line = line.strip()
|
|
if line:
|
|
cases.append(json.loads(line))
|
|
|
|
if EVAL_LIMIT > 0:
|
|
return cases[:EVAL_LIMIT]
|
|
|
|
return cases
|
|
|
|
|
|
def post_json(url: str, payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
raw = json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
|
|
|
req = urllib.request.Request(
|
|
url,
|
|
data=raw,
|
|
headers={"Content-Type": "application/json"},
|
|
method="POST",
|
|
)
|
|
|
|
try:
|
|
with urllib.request.urlopen(req, timeout=90) as resp:
|
|
body = resp.read().decode("utf-8", errors="replace")
|
|
return {
|
|
"ok": 200 <= resp.status < 300,
|
|
"status": resp.status,
|
|
"json": json.loads(body) if body else {},
|
|
"raw": body,
|
|
}
|
|
except urllib.error.HTTPError as e:
|
|
body = e.read().decode("utf-8", errors="replace")
|
|
return {
|
|
"ok": False,
|
|
"status": e.code,
|
|
"json": None,
|
|
"raw": body,
|
|
}
|
|
|
|
|
|
def cleanup_eval_data() -> None:
|
|
if not EVAL_CLEANUP or not PSQL_DATABASE_URL or psycopg is None:
|
|
return
|
|
|
|
# Limpeza defensiva: algumas tabelas não têm source_system/source_event_id.
|
|
# Por isso verificamos as colunas reais antes de construir o DELETE.
|
|
tables = [
|
|
"integration_outbox",
|
|
"tasks",
|
|
"action_runs",
|
|
"messages",
|
|
"raw_events",
|
|
]
|
|
|
|
def table_columns(cur, table: str) -> set[str]:
|
|
cur.execute(
|
|
"""
|
|
select column_name
|
|
from information_schema.columns
|
|
where table_name = %s
|
|
""",
|
|
(table,),
|
|
)
|
|
return {row[0] for row in cur.fetchall()}
|
|
|
|
with psycopg.connect(PSQL_DATABASE_URL) as conn:
|
|
with conn.cursor() as cur:
|
|
for table in tables:
|
|
cols = table_columns(cur, table)
|
|
conditions = []
|
|
|
|
if "source_system" in cols:
|
|
conditions.append("source_system like 'action_eval%'")
|
|
|
|
if "conversation_id" in cols:
|
|
conditions.append("conversation_id like 'eval-%'")
|
|
|
|
if "contact_id" in cols:
|
|
conditions.append("contact_id like 'eval-contact-%'")
|
|
|
|
if "source_event_id" in cols:
|
|
conditions.append("source_event_id like 'eval-%'")
|
|
|
|
if "idempotency_key" in cols:
|
|
conditions.append("idempotency_key like '%eval-%'")
|
|
|
|
if "metadata" in cols:
|
|
conditions.append("metadata::text like '%action_eval%'")
|
|
|
|
if not conditions:
|
|
continue
|
|
|
|
sql = f"delete from {table} where " + " or ".join(conditions)
|
|
cur.execute(sql)
|
|
|
|
conn.commit()
|
|
|
|
|
|
def main() -> int:
|
|
cases = load_cases()
|
|
|
|
cleanup_eval_data()
|
|
|
|
results = []
|
|
ok_count = 0
|
|
|
|
print(f"Dataset: {DATASET}")
|
|
print(f"Cases: {len(cases)}")
|
|
print(f"API: {API_URL}")
|
|
print(f"EVAL_LLM_ONLY={EVAL_LLM_ONLY}")
|
|
print("---")
|
|
|
|
for i, case in enumerate(cases, start=1):
|
|
conv_id = f"eval-{case['id']}"
|
|
payload = {
|
|
"last_customer_message": case["message"],
|
|
"previous_context": case.get("context", ""),
|
|
"source": "action_eval_llm_only" if EVAL_LLM_ONLY else "action_eval",
|
|
"conversation_id": conv_id,
|
|
"contact_id": f"eval-contact-{i}",
|
|
}
|
|
|
|
result = post_json(API_URL, payload)
|
|
|
|
if not result["ok"]:
|
|
got = "HTTP_ERROR"
|
|
confidence = None
|
|
provider = None
|
|
note = result["raw"][:300]
|
|
else:
|
|
data = result["json"] or {}
|
|
decision = data.get("action_decision") or {}
|
|
usage = data.get("usage") or {}
|
|
got = decision.get("action_code") or data.get("action_result", {}).get("action_code") or "UNKNOWN"
|
|
confidence = decision.get("confidence")
|
|
provider = usage.get("provider")
|
|
note = decision.get("note")
|
|
|
|
expected = case["expected"]
|
|
passed = got == expected
|
|
ok_count += 1 if passed else 0
|
|
|
|
results.append({
|
|
"id": case["id"],
|
|
"expected": expected,
|
|
"got": got,
|
|
"ok": passed,
|
|
"confidence": confidence,
|
|
"provider": provider,
|
|
"note": note,
|
|
})
|
|
|
|
status = "OK" if passed else "FAIL"
|
|
print(f"{status:4} {case['id']}")
|
|
print(f" expected={expected}")
|
|
print(f" got ={got}")
|
|
print(f" conf ={confidence} provider={provider}")
|
|
if not passed:
|
|
print(f" note ={note}")
|
|
print("---")
|
|
|
|
accuracy = ok_count / len(cases) if cases else 0
|
|
|
|
print(f"accuracy={accuracy:.1%} ({ok_count}/{len(cases)})")
|
|
|
|
by_expected = {}
|
|
for r in results:
|
|
item = by_expected.setdefault(r["expected"], {"ok": 0, "total": 0})
|
|
item["total"] += 1
|
|
item["ok"] += 1 if r["ok"] else 0
|
|
|
|
print("--- by expected")
|
|
for code, stats in sorted(by_expected.items()):
|
|
print(f"{code}: {stats['ok']}/{stats['total']}")
|
|
|
|
Path("data/action_eval_last_results.json").write_text(
|
|
json.dumps(results, ensure_ascii=False, indent=2),
|
|
encoding="utf-8",
|
|
)
|
|
|
|
cleanup_eval_data()
|
|
|
|
return 0 if accuracy >= 0.90 else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|