-
Notifications
You must be signed in to change notification settings - Fork 333
Expand file tree
/
Copy pathscore.py
More file actions
203 lines (162 loc) · 6.97 KB
/
score.py
File metadata and controls
203 lines (162 loc) · 6.97 KB
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
"""
Score each occupation's AI exposure using an LLM via OpenRouter.
Reads Markdown descriptions from pages/, sends each to an LLM with a scoring
rubric, and collects structured scores. Results are cached incrementally to
scores.json so the script can be resumed if interrupted.
Usage:
uv run python score.py
uv run python score.py --model google/gemini-3-flash-preview
uv run python score.py --start 0 --end 10 # test on first 10
"""
import argparse
import json
import os
import time
import httpx
from dotenv import load_dotenv
load_dotenv()
DEFAULT_MODEL = "google/gemini-3-flash-preview"
OUTPUT_FILE = "scores.json"
API_URL = "https://openrouter.ai/api/v1/chat/completions"
SYSTEM_PROMPT = """\
You are an expert analyst evaluating how exposed different occupations are to \
AI. You will be given a detailed description of an occupation from the Bureau \
of Labor Statistics.
Rate the occupation's overall **AI Exposure** on a scale from 0 to 10.
AI Exposure measures: how much will AI reshape this occupation? Consider both \
direct effects (AI automating tasks currently done by humans) and indirect \
effects (AI making each worker so productive that fewer are needed).
A key signal is whether the job's work product is fundamentally digital. If \
the job can be done entirely from a home office on a computer — writing, \
coding, analyzing, communicating — then AI exposure is inherently high (7+), \
because AI capabilities in digital domains are advancing rapidly. Even if \
today's AI can't handle every aspect of such a job, the trajectory is steep \
and the ceiling is very high. Conversely, jobs requiring physical presence, \
manual skill, or real-time human interaction in the physical world have a \
natural barrier to AI exposure.
Use these anchors to calibrate your score:
- **0–1: Minimal exposure.** The work is almost entirely physical, hands-on, \
or requires real-time human presence in unpredictable environments. AI has \
essentially no impact on daily work. \
Examples: roofer, landscaper, commercial diver.
- **2–3: Low exposure.** Mostly physical or interpersonal work. AI might help \
with minor peripheral tasks (scheduling, paperwork) but doesn't touch the \
core job. \
Examples: electrician, plumber, firefighter, dental hygienist.
- **4–5: Moderate exposure.** A mix of physical/interpersonal work and \
knowledge work. AI can meaningfully assist with the information-processing \
parts but a substantial share of the job still requires human presence. \
Examples: registered nurse, police officer, veterinarian.
- **6–7: High exposure.** Predominantly knowledge work with some need for \
human judgment, relationships, or physical presence. AI tools are already \
useful and workers using AI may be substantially more productive. \
Examples: teacher, manager, accountant, journalist.
- **8–9: Very high exposure.** The job is almost entirely done on a computer. \
All core tasks — writing, coding, analyzing, designing, communicating — are \
in domains where AI is rapidly improving. The occupation faces major \
restructuring. \
Examples: software developer, graphic designer, translator, data analyst, \
paralegal, copywriter.
- **10: Maximum exposure.** Routine information processing, fully digital, \
with no physical component. AI can already do most of it today. \
Examples: data entry clerk, telemarketer.
Respond with ONLY a JSON object in this exact format, no other text:
{
"exposure": <0-10>,
"rationale": "<2-3 sentences explaining the key factors>"
}\
"""
def score_occupation(client, text, model):
"""Send one occupation to the LLM and parse the structured response."""
response = client.post(
API_URL,
headers={
"Authorization": f"Bearer {os.environ['OPENROUTER_API_KEY']}",
},
json={
"model": model,
"messages": [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": text},
],
"temperature": 0.2,
},
timeout=60,
)
response.raise_for_status()
content = response.json()["choices"][0]["message"]["content"]
# Strip markdown code fences if present
content = content.strip()
if content.startswith("```"):
content = content.split("\n", 1)[1] # remove first line
if content.endswith("```"):
content = content[:-3]
content = content.strip()
return json.loads(content)
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--model", default=DEFAULT_MODEL)
parser.add_argument("--start", type=int, default=0)
parser.add_argument("--end", type=int, default=None)
parser.add_argument("--delay", type=float, default=0.5)
parser.add_argument("--force", action="store_true",
help="Re-score even if already cached")
args = parser.parse_args()
with open("occupations.json") as f:
occupations = json.load(f)
subset = occupations[args.start:args.end]
# Load existing scores
scores = {}
if os.path.exists(OUTPUT_FILE) and not args.force:
with open(OUTPUT_FILE) as f:
for entry in json.load(f):
scores[entry["slug"]] = entry
print(f"Scoring {len(subset)} occupations with {args.model}")
print(f"Already cached: {len(scores)}")
errors = []
client = httpx.Client()
for i, occ in enumerate(subset):
slug = occ["slug"]
if slug in scores:
continue
md_path = f"pages/{slug}.md"
if not os.path.exists(md_path):
print(f" [{i+1}] SKIP {slug} (no markdown)")
continue
with open(md_path) as f:
text = f.read()
print(f" [{i+1}/{len(subset)}] {occ['title']}...", end=" ", flush=True)
try:
result = score_occupation(client, text, args.model)
scores[slug] = {
"slug": slug,
"title": occ["title"],
**result,
}
print(f"exposure={result['exposure']}")
except Exception as e:
print(f"ERROR: {e}")
errors.append(slug)
# Save after each one (incremental checkpoint)
with open(OUTPUT_FILE, "w") as f:
json.dump(list(scores.values()), f, indent=2)
if i < len(subset) - 1:
time.sleep(args.delay)
client.close()
print(f"\nDone. Scored {len(scores)} occupations, {len(errors)} errors.")
if errors:
print(f"Errors: {errors}")
# Summary stats
vals = [s for s in scores.values() if "exposure" in s]
if vals:
avg = sum(s["exposure"] for s in vals) / len(vals)
by_score = {}
for s in vals:
bucket = s["exposure"]
by_score[bucket] = by_score.get(bucket, 0) + 1
print(f"\nAverage exposure across {len(vals)} occupations: {avg:.1f}")
print("Distribution:")
for k in sorted(by_score):
print(f" {k}: {'█' * by_score[k]} ({by_score[k]})")
if __name__ == "__main__":
main()