File size: 12,566 Bytes
a326604
4afca3c
a326604
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b41322c
a326604
 
 
 
 
 
 
 
d8dfdc3
ae85fb5
7d161ea
f4815d7
b41322c
d8dfdc3
a326604
 
 
b41322c
 
 
d8dfdc3
 
 
a326604
 
 
 
 
 
 
ae85fb5
 
 
 
 
 
 
a326604
 
 
 
ae85fb5
 
 
 
a326604
ae85fb5
 
a326604
 
 
1cfeb1f
47b7fef
 
1cfeb1f
47b7fef
a326604
47b7fef
 
1cfeb1f
 
47b7fef
1cfeb1f
47b7fef
1cfeb1f
 
 
 
a326604
1cfeb1f
 
 
 
 
 
 
 
 
 
 
 
a326604
7d161ea
287b290
 
 
 
 
 
 
 
a326604
 
 
 
 
 
 
b41322c
287b290
 
 
 
a326604
 
d329207
b41322c
287b290
d329207
b41322c
 
 
 
 
a326604
 
 
 
b41322c
 
 
 
d8dfdc3
 
 
a326604
 
 
95ddc4d
ae85fb5
95ddc4d
ae85fb5
287b290
 
 
 
b41322c
ae85fb5
 
 
95ddc4d
a326604
ae85fb5
1cfeb1f
 
95ddc4d
287b290
 
 
 
 
95ddc4d
 
 
 
 
 
 
 
 
ae85fb5
 
95ddc4d
a326604
ae85fb5
 
 
95ddc4d
 
a326604
ae85fb5
 
 
 
 
1cfeb1f
ae85fb5
 
 
 
1cfeb1f
7d161ea
95ddc4d
 
 
 
 
 
 
 
 
 
7d161ea
 
 
 
 
 
 
 
 
d8dfdc3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
"""
usage_logging.py
----------------
Purpose:
    This module implements privacy-preserving telemetry for the
    AI Recruiting Agent Hugging Face Space.

    Its sole purpose is to measure anonymous usage and adoption
    metrics in order to:
      - Understand how the tool is being used
      - Improve reliability and performance
      - Gauge sense of real-world adoption
      - Support research and evaluation of responsible AI practices

Privacy Principles:
    This module is explicitly designed to minimize data collection
    and avoid storing any personally identifiable information (PII).

    It DOES NOT collect or store:
      - Raw IP addresses
      - User names or Hugging Face account IDs
      - Resume contents or job descriptions
      - Emails, phone numbers, or file names
      - Full user-agent strings or device fingerprints
      - Any demographic attributes about users

    It ONLY records:
      - Approximate country and city (derived from IP, not stored)
      - UTC timestamp of the event
      - Space URL
      - High-level event type (e.g., "app_open")
      - Non-identifying, aggregate metadata (e.g., counts, booleans, latencies)

    All usage logs are:
      - Anonymized
      - Append-only
      - Persisted in a public Hugging Face Dataset repository (https://huggingface.co/datasets/19arjun89/ai_recruiting_agent_usage)
      - Versioned via immutable commit history for auditability

Ethical Safeguards:
    - Logging failures never break application functionality
    - No raw identifiers are persisted at any time
    - All telemetry is optional and best-effort
    - The system is intended for transparency and improvement,
      not for surveillance or profiling

Transparency:
    A public-facing usage reporting Space will be provided to allow
    independent verification of aggregate adoption metrics.

Author:
    Arjun Singh

Last Updated:
    2026-01-27
"""


import os
import json
from datetime import datetime
import requests
import gradio as gr
from huggingface_hub import HfApi, list_repo_files, hf_hub_download
import ipaddress
import pycountry
from io import BytesIO
import uuid
import time

SPACE_URL = "https://huggingface.co/spaces/19arjun89/AI_Recruiting_Agent"
USAGE_DATASET_REPO = "19arjun89/ai_recruiting_agent_usage"

USAGE_EVENTS_DIR = "usage/events"

LEGACY_JSONL_PATH = "usage/visits_legacy.jsonl"
ROLLUP_PATH = "usage/visits.jsonl"

def _hf_api():
    token = os.environ.get("HF_TOKEN")
    if not token:
        return None
    return HfApi(token=token)


def _is_public_ip(ip: str) -> bool:
    try:
        obj = ipaddress.ip_address(ip)
        return not (obj.is_private or obj.is_loopback or obj.is_reserved or obj.is_multicast or obj.is_link_local)
    except Exception:
        return False

def _get_client_ip(request: gr.Request) -> str:
    if request:
        xff = request.headers.get("x-forwarded-for")
        if xff:
            for part in xff.split(","):
                ip = part.strip()
                if _is_public_ip(ip):
                    return ip
        if request.client:
            host = request.client.host
            return host if _is_public_ip(host) else ""
    return ""


def _country_lookup(ip: str) -> tuple[str, str]:
    token = os.environ.get("IPINFO_TOKEN")
    if not token:
        return ("", "")

    try:
        url = f"https://ipinfo.io/{ip}/json?token={token}"
        r = requests.get(url, timeout=4)
        if r.status_code != 200:
            return ("", "")

        data = r.json()

        # Some plans: country="US"
        # Some plans: country_code="US" and country="United States"
        cc = (data.get("country_code") or data.get("country") or "").strip().upper()
        name = (data.get("country") or "").strip()

        # If name is actually a code like "US", expand it
        if len(name) == 2 and name.upper() == cc:
            name = _expand_country_code(cc)

        # If name is missing but cc exists, expand
        if not name and cc:
            name = _expand_country_code(cc)

        return (cc, name)

    except Exception:
        return ("", "")


def append_visit_to_dataset(
    country: str,
    city: str,
    event_type: str = "usage_start",
    country_source: str = "unknown",
    country_code: str = "",
    **extra_fields
):
    api = _hf_api()
    if not api:
        return

    event = {
        "ts_utc": datetime.utcnow().isoformat() + "Z",
        "space_url": SPACE_URL,
        "event": event_type,
        "country": country or "Unknown",
        "country_code": (country_code or "").strip().upper(),
        "country_source": country_source or "unknown",
        "city": city or "",
    }

    if extra_fields:
        # Prevent JSON nulls
        event.update({k: v for k, v in extra_fields.items() if v is not None})

    # Unique file path per event (prevents collisions)
    ts = datetime.utcnow().strftime("%Y%m%dT%H%M%S%f")
    uid = uuid.uuid4().hex[:8]
    path_in_repo = f"{USAGE_EVENTS_DIR}/{ts}_{uid}.json"

    try:
        api.upload_file(
            repo_id=USAGE_DATASET_REPO,
            repo_type="dataset",
            path_in_repo=path_in_repo,
            path_or_fileobj=BytesIO(json.dumps(event).encode("utf-8")),
            commit_message=f"log {event_type}",
        )
    except Exception as e:
        print("telemetry upload failed:", repr(e))



def record_visit(request: gr.Request):
    # 1) Header hint
    country_hint = _country_from_headers(request)
    if _is_valid_country_code(country_hint):
        append_visit_to_dataset(
        country=_expand_country_code(country_hint),
        city="",
        event_type="usage_start",
        country_source="header",
        country_code=country_hint.strip().upper(),
        )
        return

    # 2) IP-based lookup
    ip = _get_client_ip(request)
    if ip:
        cc, name = _country_lookup(ip)
        if _is_valid_country_code(cc):
            append_visit_to_dataset(
            country=name or _expand_country_code(cc),
            city="",
            event_type="usage_start",
            country_source="ipinfo",
            country_code=cc,
            )
        else:
            append_visit_to_dataset(
                country="Unknown",
                city="",
                event_type="usage_start",
                country_source="ipinfo_unknown",
                country_code="",
            )
        return

    # 3) Nothing usable
    append_visit_to_dataset(
        country="Unknown",
        city="",
        event_type="usage_start",
        country_source="none",
        country_code="",
    )


def _country_from_headers(request: gr.Request) -> str:
    if not request:
        return ""
    return (
        request.headers.get("cf-ipcountry") or
        request.headers.get("x-country") or
        request.headers.get("x-geo-country") or
        ""
    ).strip().upper()

def _is_valid_country_code(code: str) -> bool:
    if not code:
        return False
    code = code.strip().upper()
    # Common "unknown" markers from CDNs / proxies
    if code in {"XX", "ZZ", "UNKNOWN", "NA", "N/A", "NONE", "-"}:
        return False
    # ISO2 should be exactly 2 letters
    return len(code) == 2 and code.isalpha()


def _expand_country_code(code: str) -> str:
    if not code or len(code) != 2:
        return "Unknown"
    try:
        country = pycountry.countries.get(alpha_2=code.upper())
        return country.name if country else "Unknown"
    except Exception:
        return "Unknown"

def migrate_legacy_jsonl_to_event_files(
    max_rows: int = 100000,
    sleep_s: float = 0.0,
) -> str:
    """
    One-time migration:
    - Reads usage/visits_legacy.jsonl
    - Writes each row as its own event file under usage/events/legacy_<ts>_<n>.json
    - Skips if the legacy file doesn't exist
    - Does NOT delete legacy file (you can keep it as an archive)
    """
    api = _hf_api()
    if not api:
        return "HF_TOKEN not available. Migration requires write access."

    # 1) Download legacy JSONL from dataset repo
    try:
        legacy_local = hf_hub_download(
            repo_id=USAGE_DATASET_REPO,
            repo_type="dataset",
            filename=LEGACY_JSONL_PATH,
        )
    except Exception as e:
        return f"Legacy file not found or not accessible: {LEGACY_JSONL_PATH} ({repr(e)})"

    # 2) Read legacy rows
    rows = []
    with open(legacy_local, "r", encoding="utf-8") as f:
        for line in f:
            line = line.strip()
            if not line:
                continue
            try:
                rows.append(json.loads(line))
            except Exception:
                pass

    if not rows:
        return "Legacy file exists but contained 0 parseable rows."

    rows = rows[:max_rows]

    # 3) (Optional) check if migration already happened by looking for any legacy_* files
    try:
        files = list_repo_files(repo_id=USAGE_DATASET_REPO, repo_type="dataset")
        already = any(p.startswith(f"{USAGE_EVENTS_DIR}/legacy_") for p in files)
        if already:
            return "Migration appears to have already run (found legacy_ files in usage/events). Aborting."
    except Exception:
        # If listing fails, proceed cautiously
        pass

    # 4) Upload each row as its own event file
    uploaded = 0
    skipped = 0

    for i, evt in enumerate(rows):
        # Ensure minimal schema
        ts = (evt.get("ts_utc") or "").strip()
        if not ts:
            # If no timestamp, synthesize one to avoid empty sorting later
            ts = datetime.utcnow().isoformat() + "Z"
            evt["ts_utc"] = ts

        # Sanitize filename timestamp (avoid ":" which is annoying in filenames)
        safe_ts = (
            ts.replace(":", "")
              .replace("-", "")
              .replace(".", "")
              .replace("Z", "")
              .replace("T", "T")
        )

        path_in_repo = f"{USAGE_EVENTS_DIR}/legacy_{safe_ts}_{i:05d}.json"

        try:
            api.upload_file(
                repo_id=USAGE_DATASET_REPO,
                repo_type="dataset",
                path_in_repo=path_in_repo,
                path_or_fileobj=BytesIO(json.dumps(evt, ensure_ascii=False).encode("utf-8")),
                commit_message="migrate legacy telemetry row",
            )
            uploaded += 1
        except Exception as e:
            skipped += 1
            print("legacy migration upload failed:", path_in_repo, repr(e))

        if sleep_s:
            time.sleep(sleep_s)

    return f"Legacy migration complete. Uploaded={uploaded}, Skipped={skipped}, TotalRowsRead={len(rows)}."


def rebuild_visits_rollup_from_event_files() -> str:
    """
    Rebuilds usage/visits.jsonl from immutable per-event JSON files in usage/events/.
    This is safe if triggered manually (admin button).
    """
    api = _hf_api()
    if not api:
        return "HF_TOKEN not available. Rollup requires write access."

    # 1) List files
    try:
        files = list_repo_files(repo_id=USAGE_DATASET_REPO, repo_type="dataset")
    except Exception as e:
        return f"Could not list repo files: {repr(e)}"

    event_files = [
        f for f in files
        if f.startswith(f"{USAGE_EVENTS_DIR}/") and f.endswith(".json")
    ]
    if not event_files:
        return f"No event files found under {USAGE_EVENTS_DIR}/"

    events = []
    bad = 0

    # 2) Download & parse each event
    for path in event_files:
        try:
            local_path = hf_hub_download(
                repo_id=USAGE_DATASET_REPO,
                repo_type="dataset",
                filename=path,
            )
            with open(local_path, "r", encoding="utf-8") as f:
                events.append(json.load(f))
        except Exception:
            bad += 1

    if not events:
        return f"Found {len(event_files)} event files, but 0 were parseable (bad={bad})."

    # 3) Sort by ts_utc
    events.sort(key=lambda e: (e.get("ts_utc") or ""))

    # 4) Write JSONL
    buf = BytesIO()
    for evt in events:
        buf.write((json.dumps(evt, ensure_ascii=False) + "\n").encode("utf-8"))
    buf.seek(0)

    # 5) Upload rollup
    try:
        api.upload_file(
            repo_id=USAGE_DATASET_REPO,
            repo_type="dataset",
            path_in_repo=ROLLUP_PATH,
            path_or_fileobj=buf,
            commit_message=f"rebuild {ROLLUP_PATH} from {USAGE_EVENTS_DIR}",
        )
    except Exception as e:
        return f"Rollup upload failed: {repr(e)}"

    return f"Rollup rebuilt: {ROLLUP_PATH} rows={len(events)} (bad_files={bad})."