Spaces:
Running
Running
Sync from GitHub via hub-sync
Browse files- config.yaml +18 -0
- data/jobs_kb.json +219 -0
- data/predictions_live.jsonl +20 -16
- data/query_seed.json +7 -0
- docs/DP.md +44 -0
- docs/PRD.md +30 -1
- job_radar.py +89 -2
- run.py +27 -0
- services/job_query_agent/__init__.py +6 -0
- services/job_query_agent/audit.py +96 -0
- services/job_query_agent/discover.py +102 -0
- services/job_query_agent/evaluate.py +89 -0
- services/job_query_agent/propose.py +82 -0
- services/job_query_agent/traces.py +31 -0
- tests/test_job_query_agent.py +96 -0
- tests/test_job_radar.py +24 -0
- ui/i18n.py +10 -0
- ui/tabs/radar.py +56 -22
config.yaml
CHANGED
|
@@ -60,6 +60,24 @@ job_radar:
|
|
| 60 |
senior_retrain_cap_months: 24
|
| 61 |
min_stratified_responses: 5
|
| 62 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
# ---- job market calibration (BLS to KB displacement risk overlay) ----
|
| 64 |
job_market:
|
| 65 |
enabled: true
|
|
|
|
| 60 |
senior_retrain_cap_months: 24
|
| 61 |
min_stratified_responses: 5
|
| 62 |
|
| 63 |
+
# ---- job query calibration agent (Phase 9) ----
|
| 64 |
+
job_query_agent:
|
| 65 |
+
enabled: true
|
| 66 |
+
traces_path: data/query_agent_traces.jsonl
|
| 67 |
+
discover:
|
| 68 |
+
include_core: true
|
| 69 |
+
include_seed: true
|
| 70 |
+
seed_path: data/query_seed.json
|
| 71 |
+
include_feedback_titles: true
|
| 72 |
+
feedback_min_responses: 1
|
| 73 |
+
include_variants: false
|
| 74 |
+
max_queries_per_run: 200
|
| 75 |
+
evaluate:
|
| 76 |
+
fail_on_weak_core: true
|
| 77 |
+
review:
|
| 78 |
+
require_review: true
|
| 79 |
+
pending_dir: pending/job_calibration
|
| 80 |
+
|
| 81 |
# ---- job market calibration (BLS to KB displacement risk overlay) ----
|
| 82 |
job_market:
|
| 83 |
enabled: true
|
data/jobs_kb.json
CHANGED
|
@@ -381,6 +381,18 @@
|
|
| 381 |
"id": "tech_software_eng",
|
| 382 |
"title": "Software Engineer",
|
| 383 |
"title_zh": "软件工程师",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 384 |
"industry": "Tech",
|
| 385 |
"category": "transforming",
|
| 386 |
"description": "Creates, tests, and maintains software applications. Transitioning from manual coding to leveraging AI agents for code generation, focusing on architecture and security.",
|
|
@@ -440,6 +452,213 @@
|
|
| 440 |
}
|
| 441 |
]
|
| 442 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 443 |
{
|
| 444 |
"id": "tech_prompt_eng",
|
| 445 |
"title": "Prompt Engineer & LLM Specialist",
|
|
|
|
| 381 |
"id": "tech_software_eng",
|
| 382 |
"title": "Software Engineer",
|
| 383 |
"title_zh": "软件工程师",
|
| 384 |
+
"search_aliases": [
|
| 385 |
+
"software developer",
|
| 386 |
+
"software development",
|
| 387 |
+
"programmer",
|
| 388 |
+
"coder",
|
| 389 |
+
"full stack developer",
|
| 390 |
+
"backend developer",
|
| 391 |
+
"frontend developer",
|
| 392 |
+
"程序员",
|
| 393 |
+
"软件开发",
|
| 394 |
+
"开发工程师"
|
| 395 |
+
],
|
| 396 |
"industry": "Tech",
|
| 397 |
"category": "transforming",
|
| 398 |
"description": "Creates, tests, and maintains software applications. Transitioning from manual coding to leveraging AI agents for code generation, focusing on architecture and security.",
|
|
|
|
| 452 |
}
|
| 453 |
]
|
| 454 |
},
|
| 455 |
+
{
|
| 456 |
+
"id": "tech_product_manager",
|
| 457 |
+
"title": "Software Product Manager",
|
| 458 |
+
"title_zh": "软件产品经理",
|
| 459 |
+
"search_aliases": [
|
| 460 |
+
"product manager",
|
| 461 |
+
"产品经理",
|
| 462 |
+
"technical product manager",
|
| 463 |
+
"PM",
|
| 464 |
+
"product owner"
|
| 465 |
+
],
|
| 466 |
+
"industry": "Tech",
|
| 467 |
+
"category": "transforming",
|
| 468 |
+
"description": "Owns product vision, roadmap, and prioritization; synthesizes user research, market signals, and engineering constraints. AI copilots accelerate specs, analytics, and prototyping, but accountability for trade-offs and cross-functional alignment remains human.",
|
| 469 |
+
"description_zh": "负责产品愿景、路线图与优先级;综合用户研究、市场信号与工程约束做决策。AI 可加速需求文档、数据分析与原型,但跨团队权衡与最终责任仍依赖人类产品经理。",
|
| 470 |
+
"sensitivity": {
|
| 471 |
+
"augmentation_ratio": 0.75,
|
| 472 |
+
"demand_elasticity": 0.7,
|
| 473 |
+
"oring_leverage": 0.55,
|
| 474 |
+
"skill_distance": 0.45,
|
| 475 |
+
"diffusion_years": -0.3,
|
| 476 |
+
"absorbing_sector": 0.4,
|
| 477 |
+
"productivity_capture": 0.65,
|
| 478 |
+
"task_frontier_open": 0.75
|
| 479 |
+
},
|
| 480 |
+
"base_demand_trend": 0.07,
|
| 481 |
+
"displacement_risk": 0.22,
|
| 482 |
+
"required_skills": [
|
| 483 |
+
"Product Roadmapping",
|
| 484 |
+
"User Research Synthesis",
|
| 485 |
+
"Stakeholder Alignment",
|
| 486 |
+
"AI-Assisted Product Analytics"
|
| 487 |
+
],
|
| 488 |
+
"skill_vector": [
|
| 489 |
+
0.75,
|
| 490 |
+
0.55,
|
| 491 |
+
0.7,
|
| 492 |
+
0.6,
|
| 493 |
+
0.5,
|
| 494 |
+
0.8,
|
| 495 |
+
0.6,
|
| 496 |
+
0.55
|
| 497 |
+
],
|
| 498 |
+
"emergence_year": null,
|
| 499 |
+
"sources": [
|
| 500 |
+
"BLS: Management Occupations — Computer and Information Systems Managers (product-adjacent)",
|
| 501 |
+
"O*NET 11-3021.00 - Computer and Information Systems Managers",
|
| 502 |
+
"McKinsey: The future of product management in the age of AI"
|
| 503 |
+
],
|
| 504 |
+
"transition_targets": [
|
| 505 |
+
{
|
| 506 |
+
"target_id": "tech_prompt_eng",
|
| 507 |
+
"skill_bridge": "Develop proficiency in Prompt Design Patterns, RAG Pipeline Configuration to transition into the Prompt Engineer & LLM Specialist role.",
|
| 508 |
+
"retrain_months": 5,
|
| 509 |
+
"salary_delta": 0.12
|
| 510 |
+
},
|
| 511 |
+
{
|
| 512 |
+
"target_id": "edu_ai_learning_guide",
|
| 513 |
+
"skill_bridge": "Develop proficiency in Instructional System Design (ISD), Adaptive Learning Algorithms to transition into the AI Learning Experience Designer role.",
|
| 514 |
+
"retrain_months": 6,
|
| 515 |
+
"salary_delta": 0.1
|
| 516 |
+
},
|
| 517 |
+
{
|
| 518 |
+
"target_id": "tech_ai_infra_eng",
|
| 519 |
+
"skill_bridge": "Develop proficiency in Kubernetes (K8s) Orchestration, GPU Clustering (NCCL) to transition into the AI Infrastructure Architect role.",
|
| 520 |
+
"retrain_months": 9,
|
| 521 |
+
"salary_delta": 0.15
|
| 522 |
+
}
|
| 523 |
+
]
|
| 524 |
+
},
|
| 525 |
+
{
|
| 526 |
+
"id": "tech_data_scientist",
|
| 527 |
+
"title": "Data Scientist",
|
| 528 |
+
"title_zh": "数据科学家",
|
| 529 |
+
"search_aliases": [
|
| 530 |
+
"data science",
|
| 531 |
+
"machine learning scientist",
|
| 532 |
+
"数据科学"
|
| 533 |
+
],
|
| 534 |
+
"industry": "Tech",
|
| 535 |
+
"category": "transforming",
|
| 536 |
+
"description": "Builds statistical and machine-learning models to extract insight from data; increasingly augmented by AutoML and LLM-assisted feature engineering while human judgment remains critical for problem framing and causal interpretation.",
|
| 537 |
+
"description_zh": "构建统计与机器学习模型从数据中提取洞察;AutoML 与大模型辅助特征工程日益普及,但问题定义与因果解释仍依赖人类判断。",
|
| 538 |
+
"sensitivity": {
|
| 539 |
+
"augmentation_ratio": 0.8,
|
| 540 |
+
"demand_elasticity": 0.85,
|
| 541 |
+
"oring_leverage": 0.6,
|
| 542 |
+
"skill_distance": 0.55,
|
| 543 |
+
"diffusion_years": -0.4,
|
| 544 |
+
"absorbing_sector": 0.35,
|
| 545 |
+
"productivity_capture": 0.75,
|
| 546 |
+
"task_frontier_open": 0.85
|
| 547 |
+
},
|
| 548 |
+
"base_demand_trend": 0.1,
|
| 549 |
+
"displacement_risk": 0.18,
|
| 550 |
+
"required_skills": [
|
| 551 |
+
"Python & ML Frameworks",
|
| 552 |
+
"Experiment Design",
|
| 553 |
+
"Causal Inference",
|
| 554 |
+
"LLM Feature Pipelines"
|
| 555 |
+
],
|
| 556 |
+
"skill_vector": [
|
| 557 |
+
0.85,
|
| 558 |
+
0.8,
|
| 559 |
+
0.65,
|
| 560 |
+
0.55,
|
| 561 |
+
0.35,
|
| 562 |
+
0.45,
|
| 563 |
+
0.75,
|
| 564 |
+
0.8
|
| 565 |
+
],
|
| 566 |
+
"emergence_year": null,
|
| 567 |
+
"sources": [
|
| 568 |
+
"BLS: Data Scientists",
|
| 569 |
+
"O*NET 15-2051.00 - Data Scientists",
|
| 570 |
+
"LinkedIn Jobs on the Rise: Data Science roles"
|
| 571 |
+
],
|
| 572 |
+
"transition_targets": [
|
| 573 |
+
{
|
| 574 |
+
"target_id": "tech_prompt_eng",
|
| 575 |
+
"skill_bridge": "Develop proficiency in Prompt Design Patterns, Vector Database Operations to transition into the Prompt Engineer & LLM Specialist role.",
|
| 576 |
+
"retrain_months": 4,
|
| 577 |
+
"salary_delta": 0.14
|
| 578 |
+
},
|
| 579 |
+
{
|
| 580 |
+
"target_id": "tech_ai_infra_eng",
|
| 581 |
+
"skill_bridge": "Develop proficiency in Kubernetes (K8s) Orchestration, Distributed Storage Systems to transition into the AI Infrastructure Architect role.",
|
| 582 |
+
"retrain_months": 7,
|
| 583 |
+
"salary_delta": 0.12
|
| 584 |
+
},
|
| 585 |
+
{
|
| 586 |
+
"target_id": "fin_risk_manager",
|
| 587 |
+
"skill_bridge": "Develop proficiency in VaR Modeling, Stress Testing Frameworks to transition into the Risk Management Specialist role.",
|
| 588 |
+
"retrain_months": 8,
|
| 589 |
+
"salary_delta": 0.1
|
| 590 |
+
}
|
| 591 |
+
]
|
| 592 |
+
},
|
| 593 |
+
{
|
| 594 |
+
"id": "tech_project_manager",
|
| 595 |
+
"title": "Technical Project Manager",
|
| 596 |
+
"title_zh": "技术项目经理",
|
| 597 |
+
"search_aliases": [
|
| 598 |
+
"project manager",
|
| 599 |
+
"项目经理",
|
| 600 |
+
"TPM",
|
| 601 |
+
"program manager"
|
| 602 |
+
],
|
| 603 |
+
"industry": "Tech",
|
| 604 |
+
"category": "transforming",
|
| 605 |
+
"description": "Coordinates cross-functional engineering delivery, timelines, and risk; AI scheduling and status summarization tools reduce admin load but stakeholder negotiation and ambiguity resolution stay human-centric.",
|
| 606 |
+
"description_zh": "协调跨职能研发交付、时间表与风险;AI 排期与状态摘要工具减轻事务性负担,但利益相关方博弈与模糊情境决策仍依赖人类。",
|
| 607 |
+
"sensitivity": {
|
| 608 |
+
"augmentation_ratio": 0.65,
|
| 609 |
+
"demand_elasticity": 0.6,
|
| 610 |
+
"oring_leverage": 0.5,
|
| 611 |
+
"skill_distance": 0.4,
|
| 612 |
+
"diffusion_years": -0.2,
|
| 613 |
+
"absorbing_sector": 0.45,
|
| 614 |
+
"productivity_capture": 0.55,
|
| 615 |
+
"task_frontier_open": 0.6
|
| 616 |
+
},
|
| 617 |
+
"base_demand_trend": 0.05,
|
| 618 |
+
"displacement_risk": 0.28,
|
| 619 |
+
"required_skills": [
|
| 620 |
+
"Agile Delivery",
|
| 621 |
+
"Risk & Dependency Management",
|
| 622 |
+
"Technical Communication",
|
| 623 |
+
"AI-Assisted Planning Tools"
|
| 624 |
+
],
|
| 625 |
+
"skill_vector": [
|
| 626 |
+
0.65,
|
| 627 |
+
0.5,
|
| 628 |
+
0.6,
|
| 629 |
+
0.55,
|
| 630 |
+
0.55,
|
| 631 |
+
0.75,
|
| 632 |
+
0.55,
|
| 633 |
+
0.5
|
| 634 |
+
],
|
| 635 |
+
"emergence_year": null,
|
| 636 |
+
"sources": [
|
| 637 |
+
"BLS: Project Management Specialists",
|
| 638 |
+
"O*NET 13-1082.00 - Project Management Specialists",
|
| 639 |
+
"PMI: Pulse of the Profession"
|
| 640 |
+
],
|
| 641 |
+
"transition_targets": [
|
| 642 |
+
{
|
| 643 |
+
"target_id": "tech_product_manager",
|
| 644 |
+
"skill_bridge": "Develop proficiency in Product Roadmapping, User Research Synthesis to transition into the Software Product Manager role.",
|
| 645 |
+
"retrain_months": 6,
|
| 646 |
+
"salary_delta": 0.11
|
| 647 |
+
},
|
| 648 |
+
{
|
| 649 |
+
"target_id": "tech_software_eng",
|
| 650 |
+
"skill_bridge": "Develop proficiency in System Architecture, API Design to transition into the Software Engineer role.",
|
| 651 |
+
"retrain_months": 8,
|
| 652 |
+
"salary_delta": 0.13
|
| 653 |
+
},
|
| 654 |
+
{
|
| 655 |
+
"target_id": "log_supply_chain_analyst",
|
| 656 |
+
"skill_bridge": "Develop proficiency in Demand Forecasting, Inventory Optimization to transition into the Supply Chain Analyst role.",
|
| 657 |
+
"retrain_months": 7,
|
| 658 |
+
"salary_delta": 0.08
|
| 659 |
+
}
|
| 660 |
+
]
|
| 661 |
+
},
|
| 662 |
{
|
| 663 |
"id": "tech_prompt_eng",
|
| 664 |
"title": "Prompt Engineer & LLM Specialist",
|
data/predictions_live.jsonl
CHANGED
|
@@ -1,16 +1,20 @@
|
|
| 1 |
-
{"
|
| 2 |
-
{"
|
| 3 |
-
{"
|
| 4 |
-
{"
|
| 5 |
-
{"rationale":"This prediction is based on the recent investments and community support announcements by Google, such as the $1.5 billion investment in Alabama and the new community investments in Virginia. These efforts are likely to create new job opportunities in the AI and data science sector. I have chosen to focus on job postings rather than employment numbers to avoid the wrong_metric failure mode, as job postings can provide a more timely and sensitive indicator of labor market trends.","confidence":0.7,"resolution_date":"2027-01-15","sources":["https://www.bls.gov/jlt/","https://blog.google/innovation-and-ai/infrastructure-and-cloud/global-network/alabama-investment-june-2026/"],"status":"open","judged_rationale":"","brier":null
|
| 6 |
-
{"rationale":"This prediction is based on the recent announcements from Google, such as the launch of Gemini Omni and Gemini 3.5, which are likely to improve the efficiency and reduce the costs of cloud computing services. I have chosen to focus on the average price of cloud computing services rather than the overall market size to avoid the wrong_granularity failure mode, as prices can provide a more nuanced indicator of market trends.","confidence":0.6,"resolution_date":"2027-07-15","sources":["https://cloud.google.com/","https://blog.google/innovation-and-ai/models-and-research/gemini-models/gemini-omni-3-5-videos/"],"status":"open","judged_rationale":"","brier":null
|
| 7 |
-
{"rationale":"This prediction is based on the recent trends in AI adoption, such as the use of conversational AI systems like AMIE, which can help SMEs improve their customer service and marketing capabilities. I have chosen to focus on SMEs rather than large enterprises to avoid the wrong_segment failure mode, as SMEs are more likely to benefit from the adoption of AI-powered tools.","confidence":0.65,"resolution_date":"2027-04-15","sources":["https://blog.google/innovation-and-ai/models-and-research/google-research/amie-for-disease-management-in-nature/","https://www.nsba.com/"],"status":"open","judged_rationale":"","brier":null
|
| 8 |
-
{"rationale":"This prediction is based on the recent announcements from governments and private sector companies, such as the Federal Reserve's request for comment on a proposal to require certain payment stablecoin issuers to maintain an effective customer identification program. I have chosen to focus on the total investment in AI research and development rather than specific areas of investment to avoid the wrong_metric failure mode, as total investment can provide a broader indicator of trend.","confidence":0.75,"resolution_date":"2028-01-15","sources":["https://www.federalreserve.gov/newsevents/pressreleases/bcreg20260618a.htm","https://www.nsf.gov/"],"status":"open","judged_rationale":"","brier":null
|
| 9 |
-
{"rationale":"This prediction is grounded in the theory of augmentation vs automation, where new work emerges in areas that complement AI technology. The recent investments and community support announcements by Google in Alabama and Virginia suggest a growing demand for AI talent. To avoid the wrong_granularity failure mode, I have chosen to focus on the specific occupation of AI and machine learning engineers rather than overall developer employment.","confidence":0.7,"resolution_date":"2027-12-31","sources":["https://www.bls.gov/","https://blog.google/innovation-and-ai/infrastructure-and-cloud/global-network/alabama-investment-june-2026/"],"status":"open","judged_rationale":"","brier":null
|
| 10 |
-
{"rationale":"This prediction is based on the concept of comparative advantage, where companies that adopt AI technologies are likely to experience increased productivity and competitiveness, leading to higher valuations. The recent announcements by Google about their AI research and investments provide evidence of the growing importance of AI adoption in the business world. To avoid the wrong_attribution failure mode, I have controlled for the overall impact of macroeconomic factors by focusing on the relative performance of AI-adopting companies within the S&P 500 index.","confidence":0.6,"resolution_date":"2028-12-31","sources":["https://finance.yahoo.com/","https://blog.google/innovation-and-ai/models-and-research/gemini-models/gemini-omni-3-5-videos/"],"status":"open","judged_rationale":"","brier":null
|
| 11 |
-
{"rationale":"This prediction is based on the concept of induced demand, where the growing availability and accessibility of AI technologies leads to increased demand for education and training in these areas. The recent announcements by Google about their AI research and investments, as well as the growing interest in AI-related topics, provide evidence of the growing demand for AI education. To avoid the wrong_metric failure mode, I have chosen to focus on the specific metric of enrollment numbers in online courses rather than overall interest or engagement.","confidence":0.65,"resolution_date":"2027-12-31","sources":["https://www.coursera.org/","https://blog.google/innovation-and-ai/technology/ai/university-waterloo-labs/"],"status":"open","judged_rationale":"","brier":null
|
| 12 |
-
{"rationale":"This prediction is based on the concept of policy response to technological change, where regulatory bodies adapt to new technologies by establishing guidelines and regulations. The recent announcements by the Federal Reserve about their plans to regulate stablecoins and other digital assets provide evidence of the growing attention to AI-powered financial instruments. To avoid the wrong_horizon failure mode, I have chosen a relatively long-term horizon to allow for the development and implementation of regulatory policies.","confidence":0.55,"resolution_date":"2028-12-31","sources":["https://www.federalreserve.gov/","https://www.federalreserve.gov/newsevents/pressreleases/bcreg20260618a.htm"],"status":"open","judged_rationale":"","brier":null
|
| 13 |
-
{"rationale":"This prediction is grounded in the concept of augmentation vs automation (Autor et al.) and the job-evolution prior, which suggests a high augmentation ratio for data scientists. The increasing number of papers on AI and machine learning applications, such as those listed in the fresh evidence ingested in this cycle, supports this claim. I have chosen the occupation 'Data Scientists and Statisticians' to avoid the wrong_granularity failure mode, as it is a specific segment within the broader category of developer employment.","confidence":0.7,"resolution_date":"2028-12-31","sources":["https://www.bls.gov/"],"status":"open","judged_rationale":"","brier":null
|
| 14 |
-
{"
|
| 15 |
-
{"
|
| 16 |
-
{"rationale":"This prediction is based on the concept of comparative advantage (Kremer) and the increasing importance of R&D in driving economic growth. The growing demand for AI and machine learning technologies, as evidenced by the papers on large language models and multimodal learning, supports this claim. To avoid the wrong_granularity failure mode, I have chosen to focus on the share of GDP allocated to R&D at the national level rather than a specific industry or sector.","confidence":0.65,"resolution_date":"2030-12-31","sources":["https://www.bea.gov/"],"status":"open","judged_rationale":"","brier":null
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{"category":"labor","id":"2384fe381c30a311","statement":"By 2028-Q2, the number of jobs in the 'data scientist' category will increase by at least 15% compared to 2026-Q2, driven by the growing demand for AI-related skills.","horizon":"2028-Q2","resolution_criteria":"Comparison of the number of data scientist jobs in 2026-Q2 and 2028-Q2, using data from the Bureau of Labor Statistics (BLS) or equivalent sources.","created_at":"2026-06-23T19:36:24.629511","outcome":null,"resolved_at":null,"rationale":"The papers on AI-driven optimization, diffusion models, and neural networks suggest a growing need for professionals with expertise in AI and data analysis. According to the job-evolution prior, high augmentation ratios are associated with higher net job multipliers, which supports this prediction.","confidence":0.6,"resolution_date":"2028-06-30","sources":["https://www.bls.gov/","https://arxiv.org/abs/2606.23689v1"],"status":"open","judged_rationale":"","brier":null}
|
| 2 |
+
{"category":"macro","id":"081dfcca5494434d","statement":"The share of GDP attributed to the 'information' sector will increase by at least 2% by 2030-Q4, compared to 2026-Q4, driven by the growing adoption of AI and digital technologies.","horizon":"2030-Q4","resolution_criteria":"Comparison of the share of GDP attributed to the 'information' sector in 2026-Q4 and 2030-Q4, using data from the Bureau of Economic Analysis (BEA) or equivalent sources.","created_at":"2026-06-23T19:36:24.629754","outcome":null,"resolved_at":null,"rationale":"The papers on diffusion models, neural networks, and AI-driven optimization suggest a growing importance of digital technologies in various industries. According to the job-evolution prior, high demand elasticity is associated with higher net job multipliers, which supports this prediction.","confidence":0.55,"resolution_date":"2030-12-31","sources":["https://www.bea.gov/","https://arxiv.org/abs/2606.23670v1"],"status":"open","judged_rationale":"","brier":null}
|
| 3 |
+
{"category":"compute","id":"96013f86014aab86","statement":"By 2027-Q1, at least 20% of large language models (LLMs) will incorporate multimodal capabilities, such as text-to-image or vision-language-action, driven by the growing demand for more sophisticated AI applications.","horizon":"2027-Q1","resolution_criteria":"Survey of large language models (LLMs) and their capabilities, using data from industry reports or equivalent sources.","created_at":"2026-06-23T19:36:24.629865","outcome":null,"resolved_at":null,"rationale":"The papers on multimodal content generation, vision-language-action models, and text-to-image reward modeling suggest a growing interest in multimodal capabilities. According to the fresh evidence ingested in this cycle, there is a growing trend towards more sophisticated AI applications.","confidence":0.65,"resolution_date":"2027-03-31","sources":["https://arxiv.org/abs/2606.23643v1","https://arxiv.org/abs/2606.23617v1"],"status":"open","judged_rationale":"","brier":null}
|
| 4 |
+
{"category":"labor","id":"c911767e3dd8d6d4","statement":"By 2029-Q3, the number of jobs in the 'healthcare' sector that involve AI-assisted decision-making will increase by at least 10% compared to 2026-Q3, driven by the growing adoption of AI in healthcare.","horizon":"2029-Q3","resolution_criteria":"Comparison of the number of jobs in the 'healthcare' sector that involve AI-assisted decision-making in 2026-Q3 and 2029-Q3, using data from the Bureau of Labor Statistics (BLS) or equivalent sources.","created_at":"2026-06-23T19:36:24.629940","outcome":null,"resolved_at":null,"rationale":"The papers on AI-driven optimization of quality of recovery, AI-assisted decision-making, and neural networks suggest a growing interest in AI applications in healthcare. According to the job-evolution prior, high skill distances are associated with longer lags in job transitions, which supports this prediction.","confidence":0.5,"resolution_date":"2029-09-30","sources":["https://www.bls.gov/","https://arxiv.org/abs/2606.23631v1"],"status":"open","judged_rationale":"","brier":null}
|
| 5 |
+
{"category":"labor","id":"22b780e78d985ddc","statement":"The number of job postings in the AI and data science sector will increase by at least 15% by the end of 2026-Q4, compared to the same period in 2025.","horizon":"2026-Q4","resolution_criteria":"The statement will be judged true or false based on the data from the Bureau of Labor Statistics (BLS) on job openings and labor turnover, specifically the Job Openings and Labor Turnover Survey (JOLTS).","created_at":"2026-06-24T01:15:27.651702","outcome":null,"resolved_at":null,"rationale":"This prediction is based on the recent investments and community support announcements by Google, such as the $1.5 billion investment in Alabama and the new community investments in Virginia. These efforts are likely to create new job opportunities in the AI and data science sector. I have chosen to focus on job postings rather than employment numbers to avoid the wrong_metric failure mode, as job postings can provide a more timely and sensitive indicator of labor market trends.","confidence":0.7,"resolution_date":"2027-01-15","sources":["https://www.bls.gov/jlt/","https://blog.google/innovation-and-ai/infrastructure-and-cloud/global-network/alabama-investment-june-2026/"],"status":"open","judged_rationale":"","brier":null}
|
| 6 |
+
{"category":"compute","id":"7efc75b2f6642e67","statement":"The average price of cloud computing services will decrease by at least 5% by the end of 2027-Q2, driven by increased competition and improvements in AI-powered infrastructure management.","horizon":"2027-Q2","resolution_criteria":"The statement will be judged true or false based on the data from the cloud computing service providers, such as Amazon Web Services (AWS), Microsoft Azure, and Google Cloud Platform (GCP), as well as market research reports from firms like Gartner or Forrester.","created_at":"2026-06-24T01:15:27.651950","outcome":null,"resolved_at":null,"rationale":"This prediction is based on the recent announcements from Google, such as the launch of Gemini Omni and Gemini 3.5, which are likely to improve the efficiency and reduce the costs of cloud computing services. I have chosen to focus on the average price of cloud computing services rather than the overall market size to avoid the wrong_granularity failure mode, as prices can provide a more nuanced indicator of market trends.","confidence":0.6,"resolution_date":"2027-07-15","sources":["https://cloud.google.com/","https://blog.google/innovation-and-ai/models-and-research/gemini-models/gemini-omni-3-5-videos/"],"status":"open","judged_rationale":"","brier":null}
|
| 7 |
+
{"category":"adoption","id":"60f3fe50468e8e5a","statement":"The number of small and medium-sized enterprises (SMEs) adopting AI-powered tools for customer service and marketing will increase by at least 20% by the end of 2027-Q1, compared to the same period in 2026.","horizon":"2027-Q1","resolution_criteria":"The statement will be judged true or false based on the data from market research reports and surveys, such as those conducted by the National Small Business Association (NSBA) or the Small Business Administration (SBA).","created_at":"2026-06-24T01:15:27.652044","outcome":null,"resolved_at":null,"rationale":"This prediction is based on the recent trends in AI adoption, such as the use of conversational AI systems like AMIE, which can help SMEs improve their customer service and marketing capabilities. I have chosen to focus on SMEs rather than large enterprises to avoid the wrong_segment failure mode, as SMEs are more likely to benefit from the adoption of AI-powered tools.","confidence":0.65,"resolution_date":"2027-04-15","sources":["https://blog.google/innovation-and-ai/models-and-research/google-research/amie-for-disease-management-in-nature/","https://www.nsba.com/"],"status":"open","judged_rationale":"","brier":null}
|
| 8 |
+
{"category":"capital","id":"67f3daccc3d3429b","statement":"The total investment in AI research and development will increase by at least 10% by the end of 2027, compared to the same period in 2026, driven by government initiatives and private sector investments.","horizon":"2027","resolution_criteria":"The statement will be judged true or false based on the data from government agencies, such as the National Science Foundation (NSF), and private sector research firms, such as IDC or Gartner.","created_at":"2026-06-24T01:15:27.652114","outcome":null,"resolved_at":null,"rationale":"This prediction is based on the recent announcements from governments and private sector companies, such as the Federal Reserve's request for comment on a proposal to require certain payment stablecoin issuers to maintain an effective customer identification program. I have chosen to focus on the total investment in AI research and development rather than specific areas of investment to avoid the wrong_metric failure mode, as total investment can provide a broader indicator of trend.","confidence":0.75,"resolution_date":"2028-01-15","sources":["https://www.federalreserve.gov/newsevents/pressreleases/bcreg20260618a.htm","https://www.nsf.gov/"],"status":"open","judged_rationale":"","brier":null}
|
| 9 |
+
{"category":"labor","id":"344f076d73d5cbf9","statement":"The number of job postings for AI and machine learning engineers in the United States will increase by at least 15% year-over-year by the end of 2027","horizon":"2027","resolution_criteria":"The statement will be deemed true if the year-over-year growth in job postings for AI and machine learning engineers exceeds 15% according to data from the Bureau of Labor Statistics or a similar reputable source","created_at":"2026-06-24T01:20:01.026916","outcome":null,"resolved_at":null,"rationale":"This prediction is grounded in the theory of augmentation vs automation, where new work emerges in areas that complement AI technology. The recent investments and community support announcements by Google in Alabama and Virginia suggest a growing demand for AI talent. To avoid the wrong_granularity failure mode, I have chosen to focus on the specific occupation of AI and machine learning engineers rather than overall developer employment.","confidence":0.7,"resolution_date":"2027-12-31","sources":["https://www.bls.gov/","https://blog.google/innovation-and-ai/infrastructure-and-cloud/global-network/alabama-investment-june-2026/"],"status":"open","judged_rationale":"","brier":null}
|
| 10 |
+
{"category":"capital","id":"53c9a9c7dd85221e","statement":"The price-to-earnings ratio of companies in the S&P 500 index that have adopted AI technologies will be at least 10% higher than those that have not adopted AI by the end of 2028","horizon":"2028","resolution_criteria":"The statement will be deemed true if the average price-to-earnings ratio of AI-adopting companies in the S&P 500 index exceeds that of non-AI-adopting companies by at least 10% according to data from Yahoo Finance or a similar reputable source","created_at":"2026-06-24T01:20:01.027170","outcome":null,"resolved_at":null,"rationale":"This prediction is based on the concept of comparative advantage, where companies that adopt AI technologies are likely to experience increased productivity and competitiveness, leading to higher valuations. The recent announcements by Google about their AI research and investments provide evidence of the growing importance of AI adoption in the business world. To avoid the wrong_attribution failure mode, I have controlled for the overall impact of macroeconomic factors by focusing on the relative performance of AI-adopting companies within the S&P 500 index.","confidence":0.6,"resolution_date":"2028-12-31","sources":["https://finance.yahoo.com/","https://blog.google/innovation-and-ai/models-and-research/gemini-models/gemini-omni-3-5-videos/"],"status":"open","judged_rationale":"","brier":null}
|
| 11 |
+
{"category":"adoption","id":"6966c08193e3a916","statement":"The number of students enrolled in online courses related to AI and machine learning will increase by at least 20% year-over-year by the end of 2027","horizon":"2027","resolution_criteria":"The statement will be deemed true if the year-over-year growth in enrollment numbers for online courses related to AI and machine learning exceeds 20% according to data from online course platforms such as Coursera or edX","created_at":"2026-06-24T01:20:01.027258","outcome":null,"resolved_at":null,"rationale":"This prediction is based on the concept of induced demand, where the growing availability and accessibility of AI technologies leads to increased demand for education and training in these areas. The recent announcements by Google about their AI research and investments, as well as the growing interest in AI-related topics, provide evidence of the growing demand for AI education. To avoid the wrong_metric failure mode, I have chosen to focus on the specific metric of enrollment numbers in online courses rather than overall interest or engagement.","confidence":0.65,"resolution_date":"2027-12-31","sources":["https://www.coursera.org/","https://blog.google/innovation-and-ai/technology/ai/university-waterloo-labs/"],"status":"open","judged_rationale":"","brier":null}
|
| 12 |
+
{"category":"policy","id":"09c6211873bb0412","statement":"The Federal Reserve will establish clear guidelines for the regulation of AI-powered financial instruments by the end of 2028","horizon":"2028","resolution_criteria":"The statement will be deemed true if the Federal Reserve issues clear guidelines for the regulation of AI-powered financial instruments by the end of 2028 according to official Federal Reserve announcements or publications","created_at":"2026-06-24T01:20:01.027334","outcome":null,"resolved_at":null,"rationale":"This prediction is based on the concept of policy response to technological change, where regulatory bodies adapt to new technologies by establishing guidelines and regulations. The recent announcements by the Federal Reserve about their plans to regulate stablecoins and other digital assets provide evidence of the growing attention to AI-powered financial instruments. To avoid the wrong_horizon failure mode, I have chosen a relatively long-term horizon to allow for the development and implementation of regulatory policies.","confidence":0.55,"resolution_date":"2028-12-31","sources":["https://www.federalreserve.gov/","https://www.federalreserve.gov/newsevents/pressreleases/bcreg20260618a.htm"],"status":"open","judged_rationale":"","brier":null}
|
| 13 |
+
{"category":"labor","id":"e2e39b7dc82c9e84","statement":"The number of jobs in the 'Data Scientists and Statisticians' occupation will increase by at least 15% between 2026 and 2028, driven by the growing demand for AI and machine learning expertise.","horizon":"2028-Q4","resolution_criteria":"The Bureau of Labor Statistics (BLS) will report an increase of at least 15% in the number of employed data scientists and statisticians between 2026 and 2028.","created_at":"2026-06-24T09:00:13.724586","outcome":null,"resolved_at":null,"rationale":"This prediction is grounded in the concept of augmentation vs automation (Autor et al.) and the job-evolution prior, which suggests a high augmentation ratio for data scientists. The increasing number of papers on AI and machine learning applications, such as those listed in the fresh evidence ingested in this cycle, supports this claim. I have chosen the occupation 'Data Scientists and Statisticians' to avoid the wrong_granularity failure mode, as it is a specific segment within the broader category of developer employment.","confidence":0.7,"resolution_date":"2028-12-31","sources":["https://www.bls.gov/"],"status":"open","judged_rationale":"","brier":null}
|
| 14 |
+
{"category":"adoption","id":"f7ca42ec0d210da2","statement":"By 2027, at least 20% of the top 100 companies in the S&P 500 will have implemented some form of AI-powered decision support system, such as those described in the papers on agentic models and autonomous driving.","horizon":"2027-Q4","resolution_criteria":"A survey of the top 100 companies in the S&P 500 will reveal that at least 20% have implemented AI-powered decision support systems by the end of 2027.","created_at":"2026-06-24T09:00:13.724845","outcome":null,"resolved_at":null,"rationale":"This prediction is based on the concept of creative destruction (Schumpeter) and the increasing availability of AI technologies, as evidenced by the papers on large language models and multimodal learning. The growing demand for efficient and accurate decision-making in large corporations supports this claim. To avoid the wrong_horizon failure mode, I have chosen a relatively short time horizon of 2027.","confidence":0.6,"resolution_date":"2027-12-31","sources":["https://www.spglobal.com/"],"status":"open","judged_rationale":"","brier":null}
|
| 15 |
+
{"category":"compute","id":"84df68e1dbe33091","statement":"The average training time for large language models will decrease by at least 30% between 2026 and 2028, driven by advancements in computing hardware and software optimization techniques, such as those described in the papers on scaling laws and decentralized training.","horizon":"2028-Q4","resolution_criteria":"A report by a reputable industry analyst firm will document a decrease of at least 30% in the average training time for large language models between 2026 and 2028.","created_at":"2026-06-24T09:00:13.724931","outcome":null,"resolved_at":null,"rationale":"This prediction is grounded in the concept of induced demand (Jevons' paradox) and the rapidly evolving nature of computing technologies. The increasing demand for large language models and the growing availability of optimized training methods support this claim. To avoid the wrong_metric failure mode, I have chosen to focus on the average training time rather than a specific metric like compute hours or energy consumption.","confidence":0.8,"resolution_date":"2028-12-31","sources":["https://www.marketresearchfirm.com/"],"status":"open","judged_rationale":"","brier":null}
|
| 16 |
+
{"category":"macro","id":"9361df9db0c4e103","statement":"The share of GDP allocated to research and development (R&D) in the United States will increase by at least 1.5 percentage points between 2026 and 2030, driven by the growing demand for AI and machine learning technologies.","horizon":"2030-Q4","resolution_criteria":"The Bureau of Economic Analysis (BEA) will report an increase of at least 1.5 percentage points in the share of GDP allocated to R&D between 2026 and 2030.","created_at":"2026-06-24T09:00:13.724999","outcome":null,"resolved_at":null,"rationale":"This prediction is based on the concept of comparative advantage (Kremer) and the increasing importance of R&D in driving economic growth. The growing demand for AI and machine learning technologies, as evidenced by the papers on large language models and multimodal learning, supports this claim. To avoid the wrong_granularity failure mode, I have chosen to focus on the share of GDP allocated to R&D at the national level rather than a specific industry or sector.","confidence":0.65,"resolution_date":"2030-12-31","sources":["https://www.bea.gov/"],"status":"open","judged_rationale":"","brier":null}
|
| 17 |
+
{"category":"labor","id":"ee22977eae648df8","statement":"By 2028-Q2, the percentage of job postings in the US that require AI-related skills will exceed 25%.","horizon":"2028-Q2","resolution_criteria":"According to data from the Bureau of Labor Statistics (BLS) and online job platforms such as Indeed or LinkedIn, the percentage of job postings requiring AI-related skills will be calculated and compared to the 25% threshold.","created_at":"2026-06-24T13:35:12.146504","outcome":null,"resolved_at":null,"rationale":"This prediction is based on the observed trend of increasing demand for AI-related skills in the job market, which is driven by the growing adoption of AI technologies across various industries. The papers on AI-powered AAC interfaces, Grad Detect, and UniDrive demonstrate the expanding applications of AI, supporting this trend. By focusing on job postings, this prediction avoids the wrong_granularity failure mode by using a specific metric (percentage of job postings) rather than aggregate employment numbers.","confidence":0.6,"resolution_date":"2028-06-30","sources":["https://www.bls.gov/","https://www.indeed.com/"],"status":"open","judged_rationale":"","brier":null}
|
| 18 |
+
{"category":"compute","id":"68c141ac60d298d6","statement":"The global market size for AI-powered speech recognition technology will reach $10 billion by 2027.","horizon":"2027-Q4","resolution_criteria":"According to market research reports from firms like Grand View Research or MarketsandMarkets, the global market size for AI-powered speech recognition technology will be compared to the $10 billion threshold.","created_at":"2026-06-24T13:35:12.146791","outcome":null,"resolved_at":null,"rationale":"The growth of AI-powered speech recognition technology is driven by advancements in machine learning, natural language processing, and the increasing demand for voice-assistant devices and applications. Papers like Beyond U-Net and Task Decomposition for Efficient Annotation demonstrate the advancements in speech recognition and annotation efficiency, supporting this growth. This prediction avoids the wrong_metric failure mode by focusing on market size, a directly measurable and relevant metric for this industry.","confidence":0.7,"resolution_date":"2027-12-31","sources":["https://www.grandviewresearch.com/","https://www.marketsandmarkets.com/"],"status":"open","judged_rationale":"","brier":null}
|
| 19 |
+
{"category":"capital","id":"438d4b895c9f628c","statement":"The number of companies investing in AI research and development will increase by at least 30% annually for the next two years, leading to a significant rise in AI-related patents filed by 2029.","horizon":"2029-Q4","resolution_criteria":"According to data from the World Intellectual Property Organization (WIPO) and company registration databases, the annual growth rate of companies investing in AI R&D and the number of AI-related patents filed will be calculated and compared to the stated thresholds.","created_at":"2026-06-24T13:35:12.146893","outcome":null,"resolved_at":null,"rationale":"The increasing interest in AI applications across various sectors, combined with government initiatives and investments in AI research, will drive the growth of companies engaging in AI R&D. Papers like Large-Language-Model Discovery of Quantum LDPC Codes and Scaling Laws for Task-Specific LLM Distillation highlight the expanding scope of AI research and its potential for innovation, supporting this trend. By focusing on the rate of increase in companies and AI-related patents, this prediction avoids the wrong_horizon failure mode by considering a multi-year trend rather than a single-year snapshot.","confidence":0.65,"resolution_date":"2029-12-31","sources":["https://www.wipo.int/","https://www.companyregistrationdatabase.com/"],"status":"open","judged_rationale":"","brier":null}
|
| 20 |
+
{"category":"adoption","id":"1d52048b07048ac2","statement":"By 2030, at least 20% of higher education institutions globally will have integrated AI-powered adaptive learning systems into their curriculum, leading to a significant improvement in student outcomes.","horizon":"2030-Q4","resolution_criteria":"According to surveys and reports from educational institutions and organizations like UNESCO, the percentage of higher education institutions that have integrated AI-powered adaptive learning systems will be calculated and compared to the 20% threshold.","created_at":"2026-06-24T13:35:12.146978","outcome":null,"resolved_at":null,"rationale":"The integration of AI in education is driven by its potential to enhance learning experiences, improve student outcomes, and increase accessibility. Papers like InSight and EG-VQA demonstrate the potential of AI in education and assessment, supporting this trend. By focusing on the adoption rate of AI-powered adaptive learning systems in higher education, this prediction avoids the wrong_attribution failure mode by considering the specific context of educational institutions rather than general industry trends.","confidence":0.55,"resolution_date":"2030-12-31","sources":["https://www.unesco.org/","https://www.educationinstitutiondatabase.com/"],"status":"open","judged_rationale":"","brier":null}
|
data/query_seed.json
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{"query": "software developer", "expected_id": "tech_software_eng", "source": "seed"},
|
| 3 |
+
{"query": "software product manager", "expected_id": "tech_product_manager", "source": "seed"},
|
| 4 |
+
{"query": "data scientist", "expected_id": "tech_data_scientist", "source": "seed"},
|
| 5 |
+
{"query": "financial analyst", "expected_id": "fin_credit_analyst", "source": "seed"},
|
| 6 |
+
{"query": "nurse", "source": "seed"}
|
| 7 |
+
]
|
docs/DP.md
CHANGED
|
@@ -510,6 +510,50 @@ Add to `ui/i18n.py`: `radar_fb_experience`, `radar_match_tier_none`, `radar_matc
|
|
| 510 |
|
| 511 |
---
|
| 512 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 513 |
## 11. Security & compliance defaults
|
| 514 |
|
| 515 |
- `require_review: true` — never change default in repo
|
|
|
|
| 510 |
|
| 511 |
---
|
| 512 |
|
| 513 |
+
## 14. v0.10 Design additions (Job Query Calibration Agent)
|
| 514 |
+
|
| 515 |
+
### 14.1 Module layout
|
| 516 |
+
|
| 517 |
+
```
|
| 518 |
+
services/job_query_agent/
|
| 519 |
+
├── discover.py # core_hot, query_seed.json, JobFeedback titles
|
| 520 |
+
├── evaluate.py # tier + P0 regression classification (HR-2 pure)
|
| 521 |
+
├── propose.py # CalibrationProposal → pending/job_calibration/
|
| 522 |
+
├── traces.py # JSONL append-only
|
| 523 |
+
└── audit.py # orchestrates cycle; raises on P0 failure
|
| 524 |
+
```
|
| 525 |
+
|
| 526 |
+
### 14.2 Audit flow
|
| 527 |
+
|
| 528 |
+
```
|
| 529 |
+
discover_queries(cfg)
|
| 530 |
+
→ for each query: find_best_match + search_match_tier
|
| 531 |
+
→ evaluate_query → append_trace
|
| 532 |
+
→ optional queue_proposal (once subcommand)
|
| 533 |
+
→ fail if p0_regression or weak_core (configurable)
|
| 534 |
+
```
|
| 535 |
+
|
| 536 |
+
### 14.3 Proposal types
|
| 537 |
+
|
| 538 |
+
| type | pending example | apply (P1) |
|
| 539 |
+
|------|-----------------|------------|
|
| 540 |
+
| `alias_patch` | add `search_aliases` | patch `jobs_kb.json` |
|
| 541 |
+
| `kb_profile_new` | unknown hot query | human review + KB append |
|
| 542 |
+
| `title_alias_map` | normalize map entry | `config.yaml` or `job_radar` map |
|
| 543 |
+
|
| 544 |
+
### 14.4 CLI
|
| 545 |
+
|
| 546 |
+
```bash
|
| 547 |
+
python run.py query-agent audit # CI: exit 1 on P0 regression
|
| 548 |
+
python run.py query-agent once # audit + queue proposals
|
| 549 |
+
```
|
| 550 |
+
|
| 551 |
+
### 14.5 Config (`config.yaml`)
|
| 552 |
+
|
| 553 |
+
See `job_query_agent` block: `discover.*`, `evaluate.fail_on_weak_core`, `review.pending_dir`, `traces_path`.
|
| 554 |
+
|
| 555 |
+
---
|
| 556 |
+
|
| 557 |
## 11. Security & compliance defaults
|
| 558 |
|
| 559 |
- `require_review: true` — never change default in repo
|
docs/PRD.md
CHANGED
|
@@ -261,6 +261,32 @@ cost) and field survey lacked tenure.
|
|
| 261 |
|
| 262 |
**Explicitly out of Phase 8:** prediction-crowd contributor job-title collection; Indeed/LinkedIn scraping; real embedding model swap (stays behind `Embedder` Protocol, HR-1).
|
| 263 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 264 |
---
|
| 265 |
|
| 266 |
## 6. Success metrics
|
|
@@ -280,10 +306,12 @@ cost) and field survey lacked tenure.
|
|
| 280 |
| Search strong-match precision (manual audit, n≥20 queries) | ≥ 80% relevant anchor role (Phase 8) |
|
| 281 |
| Field survey includes experience level | HR-13 (Phase 8) |
|
| 282 |
| Transition cards respect user retrain cap when profile set | Phase 8 |
|
|
|
|
|
|
|
| 283 |
|
| 284 |
---
|
| 285 |
|
| 286 |
-
## 7. Out of scope (v0.
|
| 287 |
|
| 288 |
- Auto-publishing with `require_review: false` as default
|
| 289 |
- Financial advice positioning
|
|
@@ -291,3 +319,4 @@ cost) and field survey lacked tenure.
|
|
| 291 |
- Commercial API without BUSL commercial license
|
| 292 |
- Using raw embedding cosine (~0.7) as user-facing "similarity" without combined score + tier labels
|
| 293 |
- Collecting job title / tenure on **prediction** crowd submissions (HR-13 applies to field feedback only)
|
|
|
|
|
|
| 261 |
|
| 262 |
**Explicitly out of Phase 8:** prediction-crowd contributor job-title collection; Indeed/LinkedIn scraping; real embedding model swap (stays behind `Embedder` Protocol, HR-1).
|
| 263 |
|
| 264 |
+
### Phase 9 — Job Query Calibration Agent (v0.10, 2026-06-24)
|
| 265 |
+
|
| 266 |
+
Problem: hot-role search gaps (e.g. «software developer» ≠ Software Engineer) were caught only
|
| 267 |
+
after user reports. Need a continuous **retrieval QA loop** that discovers queries, evaluates
|
| 268 |
+
KB match quality, traces gaps, and queues calibration proposals under the review gate.
|
| 269 |
+
|
| 270 |
+
**Design principles:**
|
| 271 |
+
|
| 272 |
+
1. **Retrieval QA only** — agent calibrates search anchors / aliases / KB coverage, not transition recommendations (HR-12)
|
| 273 |
+
2. **Propose, don't silently mutate** — default `require_review: true` for KB changes (HR-5)
|
| 274 |
+
3. **CORE guard in CI** — P0 regressions fail `run.py query-agent audit`
|
| 275 |
+
|
| 276 |
+
**P0 delivered:**
|
| 277 |
+
|
| 278 |
+
- [x] `services/job_query_agent/` — `discover`, `evaluate`, `propose`, `traces`, `audit`
|
| 279 |
+
- [x] `run.py query-agent audit|once` — audit fails on P0/weak-core; `once` queues proposals to `pending/job_calibration/`
|
| 280 |
+
- [x] `config.yaml` → `job_query_agent.*`; seed file `data/query_seed.json`
|
| 281 |
+
- [x] CI step: `python run.py query-agent audit` after pytest
|
| 282 |
+
- [x] Traces: `data/query_agent_traces.jsonl`
|
| 283 |
+
|
| 284 |
+
**Phase 9 backlog:**
|
| 285 |
+
|
| 286 |
+
- [ ] P1: `query-agent apply` — merge approved `alias_patch` into `jobs_kb.json` + config title map
|
| 287 |
+
- [ ] P2: HF search log ingest + frequency-weighted gap detection
|
| 288 |
+
- [ ] P3: approved `kb_profile_new` → KB append workflow
|
| 289 |
+
|
| 290 |
---
|
| 291 |
|
| 292 |
## 6. Success metrics
|
|
|
|
| 306 |
| Search strong-match precision (manual audit, n≥20 queries) | ≥ 80% relevant anchor role (Phase 8) |
|
| 307 |
| Field survey includes experience level | HR-13 (Phase 8) |
|
| 308 |
| Transition cards respect user retrain cap when profile set | Phase 8 |
|
| 309 |
+
| `query-agent audit` passes in CI (CORE + seed) | Phase 9 |
|
| 310 |
+
| LLM fallback rate on top queries (logged) | Phase 9 P2 |
|
| 311 |
|
| 312 |
---
|
| 313 |
|
| 314 |
+
## 7. Out of scope (v0.10)
|
| 315 |
|
| 316 |
- Auto-publishing with `require_review: false` as default
|
| 317 |
- Financial advice positioning
|
|
|
|
| 319 |
- Commercial API without BUSL commercial license
|
| 320 |
- Using raw embedding cosine (~0.7) as user-facing "similarity" without combined score + tier labels
|
| 321 |
- Collecting job title / tenure on **prediction** crowd submissions (HR-13 applies to field feedback only)
|
| 322 |
+
- Indeed/LinkedIn job scraping for query-agent discovery (BLS/O*NET + logs + feedback only)
|
job_radar.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
|
|
| 1 |
import json
|
| 2 |
import math
|
| 3 |
import os
|
|
@@ -62,13 +63,67 @@ def filter_by_industry(jobs: list[dict], industry: str) -> list[dict]:
|
|
| 62 |
return jobs
|
| 63 |
return [j for j in jobs if j.get("industry") == industry]
|
| 64 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
# Larger hashing dim than the crowd default (256) to avoid bucket collisions that
|
| 66 |
# made unrelated jobs spuriously match short queries like "finance".
|
| 67 |
_SEARCH_EMBED_DIM = 8192
|
| 68 |
|
| 69 |
|
| 70 |
def _default_embedder():
|
| 71 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
|
| 73 |
|
| 74 |
def _job_embed_text(job: dict) -> str:
|
|
@@ -78,9 +133,11 @@ def _job_embed_text(job: dict) -> str:
|
|
| 78 |
roles whose titles don't literally contain the word (e.g. "Credit Analyst").
|
| 79 |
"""
|
| 80 |
skills_str = ", ".join(job.get("required_skills", []))
|
|
|
|
| 81 |
parts = [
|
| 82 |
job.get("title", ""),
|
| 83 |
job.get("title_zh", ""),
|
|
|
|
| 84 |
job.get("industry", ""),
|
| 85 |
job.get("category", ""),
|
| 86 |
job.get("description", ""),
|
|
@@ -97,9 +154,37 @@ _KNOWN_INDUSTRIES = (
|
|
| 97 |
)
|
| 98 |
_QUERY_STOPWORDS = frozenset({"and", "the", "for", "with", "from", "role", "jobs"})
|
| 99 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
|
| 101 |
def _query_tokens(query: str) -> list[str]:
|
| 102 |
-
tokens = [t for t in
|
|
|
|
| 103 |
return [t for t in tokens if t not in _QUERY_STOPWORDS]
|
| 104 |
|
| 105 |
|
|
@@ -219,6 +304,7 @@ def _apply_search_scores(
|
|
| 219 |
embedder=None,
|
| 220 |
search_cfg: dict | None = None,
|
| 221 |
) -> list[dict]:
|
|
|
|
| 222 |
if embedder is None:
|
| 223 |
embedder = _default_embedder()
|
| 224 |
q_emb = embedder.embed([query])[0]
|
|
@@ -646,6 +732,7 @@ def find_best_match(
|
|
| 646 |
if not jobs or not query:
|
| 647 |
return 0.0, None
|
| 648 |
|
|
|
|
| 649 |
cfg = search_cfg or _DEFAULT_SEARCH_CONFIG
|
| 650 |
if embedder is None:
|
| 651 |
embedder = _default_embedder()
|
|
|
|
| 1 |
+
import hashlib
|
| 2 |
import json
|
| 3 |
import math
|
| 4 |
import os
|
|
|
|
| 63 |
return jobs
|
| 64 |
return [j for j in jobs if j.get("industry") == industry]
|
| 65 |
|
| 66 |
+
# Canonical hot roles that must hit the KB strongly (HR-7 coverage guardrail).
|
| 67 |
+
CORE_HOT_ROLE_QUERIES: tuple[tuple[str, str], ...] = (
|
| 68 |
+
("software product manager", "tech_product_manager"),
|
| 69 |
+
("product manager", "tech_product_manager"),
|
| 70 |
+
("产品经理", "tech_product_manager"),
|
| 71 |
+
("software developer", "tech_software_eng"),
|
| 72 |
+
("software engineer", "tech_software_eng"),
|
| 73 |
+
("程序员", "tech_software_eng"),
|
| 74 |
+
("data scientist", "tech_data_scientist"),
|
| 75 |
+
("project manager", "tech_project_manager"),
|
| 76 |
+
)
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def assert_core_hot_role_coverage(
|
| 80 |
+
jobs: list[dict] | None = None,
|
| 81 |
+
search_cfg: dict | None = None,
|
| 82 |
+
) -> None:
|
| 83 |
+
"""Raise AssertionError if a core role query misses its expected KB id."""
|
| 84 |
+
pool = jobs if jobs is not None else load_knowledge_base()
|
| 85 |
+
cfg = search_cfg or _DEFAULT_SEARCH_CONFIG
|
| 86 |
+
weak = float(cfg["tier_weak"])
|
| 87 |
+
for query, expected_id in CORE_HOT_ROLE_QUERIES:
|
| 88 |
+
sim, best = find_best_match(query, pool, search_cfg=cfg)
|
| 89 |
+
assert best is not None, f"no match for {query!r}"
|
| 90 |
+
assert best["id"] == expected_id, (
|
| 91 |
+
f"{query!r} → {best.get('id')} (expected {expected_id}, sim={sim})"
|
| 92 |
+
)
|
| 93 |
+
assert sim >= weak, f"{query!r} sim {sim} < tier_weak {weak}"
|
| 94 |
+
|
| 95 |
+
|
| 96 |
# Larger hashing dim than the crowd default (256) to avoid bucket collisions that
|
| 97 |
# made unrelated jobs spuriously match short queries like "finance".
|
| 98 |
_SEARCH_EMBED_DIM = 8192
|
| 99 |
|
| 100 |
|
| 101 |
def _default_embedder():
|
| 102 |
+
return RadarHashingEmbedder(dim=_SEARCH_EMBED_DIM)
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
_SEARCH_TOKEN = re.compile(r"[a-z0-9]+|[\u4e00-\u9fff]{2,}")
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
def _l2(vec: list[float]) -> list[float]:
|
| 109 |
+
norm = math.sqrt(sum(x * x for x in vec)) or 1.0
|
| 110 |
+
return [x / norm for x in vec]
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
class RadarHashingEmbedder(HashingEmbedder):
|
| 114 |
+
"""Hashing embedder with CJK token support for bilingual job search."""
|
| 115 |
+
|
| 116 |
+
def embed(self, texts: list[str]) -> list[list[float]]:
|
| 117 |
+
out = []
|
| 118 |
+
for text in texts:
|
| 119 |
+
v = [0.0] * self.dim
|
| 120 |
+
for tok in _SEARCH_TOKEN.findall(text.lower()):
|
| 121 |
+
if tok in _QUERY_STOPWORDS and tok.isascii():
|
| 122 |
+
continue
|
| 123 |
+
bucket = int.from_bytes(hashlib.md5(tok.encode()).digest()[:4], "big")
|
| 124 |
+
v[bucket % self.dim] += 1.0
|
| 125 |
+
out.append(_l2(v))
|
| 126 |
+
return out
|
| 127 |
|
| 128 |
|
| 129 |
def _job_embed_text(job: dict) -> str:
|
|
|
|
| 133 |
roles whose titles don't literally contain the word (e.g. "Credit Analyst").
|
| 134 |
"""
|
| 135 |
skills_str = ", ".join(job.get("required_skills", []))
|
| 136 |
+
aliases_str = " ".join(job.get("search_aliases", []))
|
| 137 |
parts = [
|
| 138 |
job.get("title", ""),
|
| 139 |
job.get("title_zh", ""),
|
| 140 |
+
aliases_str,
|
| 141 |
job.get("industry", ""),
|
| 142 |
job.get("category", ""),
|
| 143 |
job.get("description", ""),
|
|
|
|
| 154 |
)
|
| 155 |
_QUERY_STOPWORDS = frozenset({"and", "the", "for", "with", "from", "role", "jobs"})
|
| 156 |
|
| 157 |
+
# Normalize common alternate job titles before retrieval (HR-7 hot-role coverage).
|
| 158 |
+
_QUERY_TITLE_ALIASES: dict[str, str] = {
|
| 159 |
+
"software developer": "software engineer",
|
| 160 |
+
"software development": "software engineer",
|
| 161 |
+
"programmer": "software engineer",
|
| 162 |
+
"coder": "software engineer",
|
| 163 |
+
"full stack developer": "software engineer",
|
| 164 |
+
"backend developer": "software engineer",
|
| 165 |
+
"frontend developer": "software engineer",
|
| 166 |
+
"dev": "software engineer",
|
| 167 |
+
"product owner": "product manager",
|
| 168 |
+
"technical product manager": "product manager",
|
| 169 |
+
"tpm": "project manager",
|
| 170 |
+
"program manager": "project manager",
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
def normalize_search_query(query: str) -> str:
|
| 175 |
+
"""Map high-traffic alternate titles to canonical KB search phrases."""
|
| 176 |
+
q = (query or "").strip()
|
| 177 |
+
if not q:
|
| 178 |
+
return q
|
| 179 |
+
key = q.lower()
|
| 180 |
+
if key in _QUERY_TITLE_ALIASES:
|
| 181 |
+
return _QUERY_TITLE_ALIASES[key]
|
| 182 |
+
return q
|
| 183 |
+
|
| 184 |
|
| 185 |
def _query_tokens(query: str) -> list[str]:
|
| 186 |
+
tokens = [t for t in _SEARCH_TOKEN.findall(query.lower()) if len(t) >= 2]
|
| 187 |
+
tokens = [t for t in tokens if not (t.isascii() and len(t) < 3)]
|
| 188 |
return [t for t in tokens if t not in _QUERY_STOPWORDS]
|
| 189 |
|
| 190 |
|
|
|
|
| 304 |
embedder=None,
|
| 305 |
search_cfg: dict | None = None,
|
| 306 |
) -> list[dict]:
|
| 307 |
+
query = normalize_search_query(query)
|
| 308 |
if embedder is None:
|
| 309 |
embedder = _default_embedder()
|
| 310 |
q_emb = embedder.embed([query])[0]
|
|
|
|
| 732 |
if not jobs or not query:
|
| 733 |
return 0.0, None
|
| 734 |
|
| 735 |
+
query = normalize_search_query(query)
|
| 736 |
cfg = search_cfg or _DEFAULT_SEARCH_CONFIG
|
| 737 |
if embedder is None:
|
| 738 |
embedder = _default_embedder()
|
run.py
CHANGED
|
@@ -15,6 +15,8 @@
|
|
| 15 |
python run.py verify-export # assert DB live state == committed JSONL (HR-11)
|
| 16 |
python run.py warmup # import predictions_live.jsonl → DB (cache-miss recovery)
|
| 17 |
python run.py warmup --src path.jsonl
|
|
|
|
|
|
|
| 18 |
"""
|
| 19 |
from __future__ import annotations
|
| 20 |
|
|
@@ -144,6 +146,28 @@ def cmd_verify_export(
|
|
| 144 |
print(f"verify-export OK ({len(db_live)} live prediction(s) in sync)")
|
| 145 |
|
| 146 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
def cmd_approve(cfg):
|
| 148 |
pending = Path("pending")
|
| 149 |
mds = sorted(pending.glob("*.md")) if pending.exists() else []
|
|
@@ -221,6 +245,9 @@ def main():
|
|
| 221 |
elif cmd == "warmup":
|
| 222 |
src = _extract_opt(args, "--src") or out_path or "data/predictions_live.jsonl"
|
| 223 |
cmd_warmup(cfg, src)
|
|
|
|
|
|
|
|
|
|
| 224 |
else:
|
| 225 |
print(__doc__)
|
| 226 |
sys.exit(1)
|
|
|
|
| 15 |
python run.py verify-export # assert DB live state == committed JSONL (HR-11)
|
| 16 |
python run.py warmup # import predictions_live.jsonl → DB (cache-miss recovery)
|
| 17 |
python run.py warmup --src path.jsonl
|
| 18 |
+
python run.py query-agent audit # Phase 9: job search calibration audit (CI)
|
| 19 |
+
python run.py query-agent once # audit + JSON summary (non-failing weak gaps)
|
| 20 |
"""
|
| 21 |
from __future__ import annotations
|
| 22 |
|
|
|
|
| 146 |
print(f"verify-export OK ({len(db_live)} live prediction(s) in sync)")
|
| 147 |
|
| 148 |
|
| 149 |
+
def cmd_query_agent(cfg, *, subcmd: str = "audit"):
|
| 150 |
+
from services.job_query_agent.audit import run_audit
|
| 151 |
+
|
| 152 |
+
agent_cfg = cfg.setdefault("job_query_agent", {})
|
| 153 |
+
if subcmd == "audit":
|
| 154 |
+
summary = run_audit(cfg, write_traces=True, queue_proposals=False)
|
| 155 |
+
print(json.dumps(summary, indent=2))
|
| 156 |
+
elif subcmd == "once":
|
| 157 |
+
# Informational run: still fails on P0 regression, logs gaps without CI noise
|
| 158 |
+
fail_weak = agent_cfg.get("evaluate", {}).get("fail_on_weak_core", True)
|
| 159 |
+
agent_cfg.setdefault("evaluate", {})["fail_on_weak_core"] = fail_weak
|
| 160 |
+
try:
|
| 161 |
+
summary = run_audit(cfg, write_traces=True, queue_proposals=True)
|
| 162 |
+
except AssertionError as exc:
|
| 163 |
+
print(str(exc), file=sys.stderr)
|
| 164 |
+
sys.exit(1)
|
| 165 |
+
print(json.dumps(summary, indent=2))
|
| 166 |
+
else:
|
| 167 |
+
print(f"unknown query-agent subcommand: {subcmd}", file=sys.stderr)
|
| 168 |
+
sys.exit(2)
|
| 169 |
+
|
| 170 |
+
|
| 171 |
def cmd_approve(cfg):
|
| 172 |
pending = Path("pending")
|
| 173 |
mds = sorted(pending.glob("*.md")) if pending.exists() else []
|
|
|
|
| 245 |
elif cmd == "warmup":
|
| 246 |
src = _extract_opt(args, "--src") or out_path or "data/predictions_live.jsonl"
|
| 247 |
cmd_warmup(cfg, src)
|
| 248 |
+
elif cmd == "query-agent":
|
| 249 |
+
sub = args[1] if len(args) > 1 else "audit"
|
| 250 |
+
cmd_query_agent(cfg, subcmd=sub)
|
| 251 |
else:
|
| 252 |
print(__doc__)
|
| 253 |
sys.exit(1)
|
services/job_query_agent/__init__.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Job Query Calibration Agent — discover search queries, evaluate retrieval, trace gaps."""
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
from services.job_query_agent.audit import run_audit
|
| 5 |
+
|
| 6 |
+
__all__ = ["run_audit"]
|
services/job_query_agent/audit.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Run a full query-agent audit cycle."""
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import json
|
| 5 |
+
from typing import Any
|
| 6 |
+
|
| 7 |
+
import job_radar
|
| 8 |
+
from services.job_query_agent.discover import discover_queries
|
| 9 |
+
from services.job_query_agent.evaluate import QueryVerdict, evaluate_query
|
| 10 |
+
from services.job_query_agent.propose import propose_from_verdict, queue_proposal
|
| 11 |
+
from services.job_query_agent.traces import append_trace
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def run_audit(
|
| 15 |
+
cfg: dict[str, Any],
|
| 16 |
+
*,
|
| 17 |
+
write_traces: bool = True,
|
| 18 |
+
queue_proposals: bool = False,
|
| 19 |
+
) -> dict[str, Any]:
|
| 20 |
+
"""Discover queries, evaluate retrieval, optionally trace and queue fixes.
|
| 21 |
+
|
| 22 |
+
Returns a summary dict. Raises AssertionError when P0 regressions exist
|
| 23 |
+
(used by CI ``run.py query-agent audit``).
|
| 24 |
+
"""
|
| 25 |
+
agent_cfg = cfg.get("job_query_agent", {})
|
| 26 |
+
job_radar_cfg = cfg.get("job_radar", {})
|
| 27 |
+
kb_path = job_radar_cfg.get("kb_path", "data/jobs_kb.json")
|
| 28 |
+
jobs = job_radar.load_knowledge_base(kb_path)
|
| 29 |
+
|
| 30 |
+
discovered = discover_queries(cfg)
|
| 31 |
+
verdicts: list[QueryVerdict] = []
|
| 32 |
+
proposals_queued = 0
|
| 33 |
+
|
| 34 |
+
for item in discovered:
|
| 35 |
+
verdict = evaluate_query(item, jobs, job_radar_cfg=job_radar_cfg)
|
| 36 |
+
verdicts.append(verdict)
|
| 37 |
+
|
| 38 |
+
if write_traces:
|
| 39 |
+
traces_path = agent_cfg.get("traces_path", "data/query_agent_traces.jsonl")
|
| 40 |
+
append_trace(traces_path, {
|
| 41 |
+
"query": verdict.query,
|
| 42 |
+
"source": verdict.source,
|
| 43 |
+
"status": verdict.status,
|
| 44 |
+
"sim": verdict.sim,
|
| 45 |
+
"tier": verdict.tier,
|
| 46 |
+
"expected_id": verdict.expected_id,
|
| 47 |
+
"best_id": verdict.best_id,
|
| 48 |
+
"message": verdict.message,
|
| 49 |
+
})
|
| 50 |
+
|
| 51 |
+
if queue_proposals and not verdict.ok:
|
| 52 |
+
proposal = propose_from_verdict(verdict)
|
| 53 |
+
if proposal:
|
| 54 |
+
pending = agent_cfg.get("review", {}).get(
|
| 55 |
+
"pending_dir", "pending/job_calibration",
|
| 56 |
+
)
|
| 57 |
+
queue_proposal(proposal, pending)
|
| 58 |
+
proposals_queued += 1
|
| 59 |
+
|
| 60 |
+
regressions = [v for v in verdicts if v.is_regression]
|
| 61 |
+
weak_core = [v for v in verdicts if v.status == "weak_core"]
|
| 62 |
+
kb_gaps = [v for v in verdicts if v.status == "kb_gap"]
|
| 63 |
+
weak_matches = [v for v in verdicts if v.status == "weak_match"]
|
| 64 |
+
ok_count = sum(1 for v in verdicts if v.ok)
|
| 65 |
+
|
| 66 |
+
summary = {
|
| 67 |
+
"queries": len(verdicts),
|
| 68 |
+
"ok": ok_count,
|
| 69 |
+
"p0_regressions": len(regressions),
|
| 70 |
+
"weak_core": len(weak_core),
|
| 71 |
+
"kb_gaps": len(kb_gaps),
|
| 72 |
+
"weak_matches": len(weak_matches),
|
| 73 |
+
"proposals_queued": proposals_queued,
|
| 74 |
+
"failures": [
|
| 75 |
+
{"query": v.query, "message": v.message, "status": v.status}
|
| 76 |
+
for v in regressions + weak_core
|
| 77 |
+
],
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
if regressions:
|
| 81 |
+
lines = "\n".join(f" - {v.query}: {v.message}" for v in regressions)
|
| 82 |
+
raise AssertionError(
|
| 83 |
+
f"Query agent P0 regression ({len(regressions)}):\n{lines}"
|
| 84 |
+
)
|
| 85 |
+
|
| 86 |
+
if weak_core and agent_cfg.get("evaluate", {}).get("fail_on_weak_core", True):
|
| 87 |
+
lines = "\n".join(f" - {v.query}: {v.message}" for v in weak_core)
|
| 88 |
+
raise AssertionError(
|
| 89 |
+
f"Query agent weak core match ({len(weak_core)}):\n{lines}"
|
| 90 |
+
)
|
| 91 |
+
|
| 92 |
+
return summary
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def audit_report_json(cfg: dict[str, Any]) -> str:
|
| 96 |
+
return json.dumps(run_audit(cfg), indent=2)
|
services/job_query_agent/discover.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Discover candidate job-search queries from core guards, seeds, and feedback."""
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import json
|
| 5 |
+
from dataclasses import dataclass
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
from typing import Any
|
| 8 |
+
|
| 9 |
+
import job_radar
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
@dataclass(frozen=True)
|
| 13 |
+
class DiscoveredQuery:
|
| 14 |
+
query: str
|
| 15 |
+
source: str
|
| 16 |
+
expected_id: str | None = None
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def _variant_queries(query: str) -> list[str]:
|
| 20 |
+
"""Generate cheap orthographic variants for audit coverage."""
|
| 21 |
+
base = query.strip()
|
| 22 |
+
if not base:
|
| 23 |
+
return []
|
| 24 |
+
variants = {base, base.lower(), base.title()}
|
| 25 |
+
if base.islower():
|
| 26 |
+
variants.add(base.upper())
|
| 27 |
+
return sorted(variants)
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def discover_from_core() -> list[DiscoveredQuery]:
|
| 31 |
+
out: list[DiscoveredQuery] = []
|
| 32 |
+
for query, expected_id in job_radar.CORE_HOT_ROLE_QUERIES:
|
| 33 |
+
out.append(DiscoveredQuery(query, "core_hot", expected_id))
|
| 34 |
+
return out
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def discover_from_seed(seed_path: str | Path) -> list[DiscoveredQuery]:
|
| 38 |
+
path = Path(seed_path)
|
| 39 |
+
if not path.is_file():
|
| 40 |
+
return []
|
| 41 |
+
raw = json.loads(path.read_text(encoding="utf-8"))
|
| 42 |
+
out: list[DiscoveredQuery] = []
|
| 43 |
+
for row in raw:
|
| 44 |
+
if isinstance(row, str):
|
| 45 |
+
out.append(DiscoveredQuery(row, "seed"))
|
| 46 |
+
continue
|
| 47 |
+
if not isinstance(row, dict) or not row.get("query"):
|
| 48 |
+
continue
|
| 49 |
+
out.append(DiscoveredQuery(
|
| 50 |
+
str(row["query"]),
|
| 51 |
+
str(row.get("source", "seed")),
|
| 52 |
+
row.get("expected_id"),
|
| 53 |
+
))
|
| 54 |
+
return out
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def discover_from_feedback(*, min_count: int = 1) -> list[DiscoveredQuery]:
|
| 58 |
+
"""Titles users submitted in JobFeedback (offline-safe: returns [] if DB empty)."""
|
| 59 |
+
try:
|
| 60 |
+
metrics = job_radar.get_empirical_metrics()
|
| 61 |
+
except Exception:
|
| 62 |
+
return []
|
| 63 |
+
out: list[DiscoveredQuery] = []
|
| 64 |
+
for title, m in metrics.items():
|
| 65 |
+
if int(m.get("total_responses", 0)) >= min_count and title.strip():
|
| 66 |
+
out.append(DiscoveredQuery(title.strip(), "feedback"))
|
| 67 |
+
return out
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def discover_queries(cfg: dict[str, Any]) -> list[DiscoveredQuery]:
|
| 71 |
+
"""Merge and dedupe discovery sources per config."""
|
| 72 |
+
agent_cfg = cfg.get("job_query_agent", {})
|
| 73 |
+
discover_cfg = agent_cfg.get("discover", {})
|
| 74 |
+
seen: set[str] = set()
|
| 75 |
+
merged: list[DiscoveredQuery] = []
|
| 76 |
+
|
| 77 |
+
def _add(items: list[DiscoveredQuery]) -> None:
|
| 78 |
+
for item in items:
|
| 79 |
+
key = item.query.strip().lower()
|
| 80 |
+
if not key or key in seen:
|
| 81 |
+
continue
|
| 82 |
+
seen.add(key)
|
| 83 |
+
merged.append(item)
|
| 84 |
+
|
| 85 |
+
if discover_cfg.get("include_core", True):
|
| 86 |
+
_add(discover_from_core())
|
| 87 |
+
seed_path = discover_cfg.get("seed_path", "data/query_seed.json")
|
| 88 |
+
if discover_cfg.get("include_seed", True):
|
| 89 |
+
_add(discover_from_seed(seed_path))
|
| 90 |
+
if discover_cfg.get("include_feedback_titles", True):
|
| 91 |
+
_add(discover_from_feedback(
|
| 92 |
+
min_count=int(discover_cfg.get("feedback_min_responses", 1)),
|
| 93 |
+
))
|
| 94 |
+
if discover_cfg.get("include_variants", True):
|
| 95 |
+
base = list(merged)
|
| 96 |
+
for item in base:
|
| 97 |
+
for variant in _variant_queries(item.query):
|
| 98 |
+
if variant.lower() != item.query.lower():
|
| 99 |
+
_add([DiscoveredQuery(variant, "variant", item.expected_id)])
|
| 100 |
+
|
| 101 |
+
max_q = int(discover_cfg.get("max_queries_per_run", 200))
|
| 102 |
+
return merged[:max_q]
|
services/job_query_agent/evaluate.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Pure evaluation of a single job-search query against the KB (HR-2)."""
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
from dataclasses import dataclass
|
| 5 |
+
from typing import Any
|
| 6 |
+
|
| 7 |
+
import job_radar
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
@dataclass(frozen=True)
|
| 11 |
+
class QueryVerdict:
|
| 12 |
+
query: str
|
| 13 |
+
source: str
|
| 14 |
+
expected_id: str | None
|
| 15 |
+
best_id: str | None
|
| 16 |
+
best_title: str | None
|
| 17 |
+
sim: float
|
| 18 |
+
tier: str
|
| 19 |
+
status: str
|
| 20 |
+
message: str
|
| 21 |
+
|
| 22 |
+
@property
|
| 23 |
+
def ok(self) -> bool:
|
| 24 |
+
return self.status == "ok"
|
| 25 |
+
|
| 26 |
+
@property
|
| 27 |
+
def is_regression(self) -> bool:
|
| 28 |
+
return self.status == "p0_regression"
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def _core_expected_map() -> dict[str, str]:
|
| 32 |
+
return {q: eid for q, eid in job_radar.CORE_HOT_ROLE_QUERIES}
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def evaluate_query(
|
| 36 |
+
discovered,
|
| 37 |
+
jobs: list[dict],
|
| 38 |
+
*,
|
| 39 |
+
job_radar_cfg: dict[str, Any] | None = None,
|
| 40 |
+
) -> QueryVerdict:
|
| 41 |
+
"""Score one query and classify the retrieval outcome."""
|
| 42 |
+
search_cfg = job_radar.resolve_search_config(job_radar_cfg)
|
| 43 |
+
tier_no = float(search_cfg["tier_no_match"])
|
| 44 |
+
tier_weak = float(search_cfg["tier_weak"])
|
| 45 |
+
|
| 46 |
+
sim, best = job_radar.find_best_match(
|
| 47 |
+
discovered.query, jobs, search_cfg=search_cfg,
|
| 48 |
+
)
|
| 49 |
+
tier = job_radar.search_match_tier(sim, search_cfg)
|
| 50 |
+
best_id = best.get("id") if best else None
|
| 51 |
+
best_title = best.get("title") if best else None
|
| 52 |
+
expected = discovered.expected_id or _core_expected_map().get(discovered.query)
|
| 53 |
+
|
| 54 |
+
if expected:
|
| 55 |
+
if best_id != expected:
|
| 56 |
+
return QueryVerdict(
|
| 57 |
+
discovered.query, discovered.source, expected, best_id, best_title,
|
| 58 |
+
sim, tier, "p0_regression",
|
| 59 |
+
f"expected {expected}, got {best_id} (sim={sim:.3f})",
|
| 60 |
+
)
|
| 61 |
+
if sim < tier_weak:
|
| 62 |
+
return QueryVerdict(
|
| 63 |
+
discovered.query, discovered.source, expected, best_id, best_title,
|
| 64 |
+
sim, tier, "weak_core",
|
| 65 |
+
f"correct id but sim {sim:.3f} < tier_weak {tier_weak}",
|
| 66 |
+
)
|
| 67 |
+
return QueryVerdict(
|
| 68 |
+
discovered.query, discovered.source, expected, best_id, best_title,
|
| 69 |
+
sim, tier, "ok", "core guard satisfied",
|
| 70 |
+
)
|
| 71 |
+
|
| 72 |
+
if tier == "none" or sim < tier_no:
|
| 73 |
+
return QueryVerdict(
|
| 74 |
+
discovered.query, discovered.source, None, best_id, best_title,
|
| 75 |
+
sim, tier, "kb_gap",
|
| 76 |
+
f"no confident KB match (sim={sim:.3f})",
|
| 77 |
+
)
|
| 78 |
+
|
| 79 |
+
if tier == "weak":
|
| 80 |
+
return QueryVerdict(
|
| 81 |
+
discovered.query, discovered.source, None, best_id, best_title,
|
| 82 |
+
sim, tier, "weak_match",
|
| 83 |
+
f"weak match to {best_title!r}",
|
| 84 |
+
)
|
| 85 |
+
|
| 86 |
+
return QueryVerdict(
|
| 87 |
+
discovered.query, discovered.source, None, best_id, best_title,
|
| 88 |
+
sim, tier, "ok", f"strong match to {best_title!r}",
|
| 89 |
+
)
|
services/job_query_agent/propose.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Propose calibration actions from query verdicts (queued, not auto-applied by default)."""
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import json
|
| 5 |
+
import re
|
| 6 |
+
from dataclasses import asdict, dataclass
|
| 7 |
+
from datetime import datetime, timezone
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
from typing import Any
|
| 10 |
+
|
| 11 |
+
from services.job_query_agent.evaluate import QueryVerdict
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
@dataclass(frozen=True)
|
| 15 |
+
class CalibrationProposal:
|
| 16 |
+
proposal_id: str
|
| 17 |
+
type: str
|
| 18 |
+
query: str
|
| 19 |
+
target_id: str | None
|
| 20 |
+
payload: dict[str, Any]
|
| 21 |
+
evidence: dict[str, Any]
|
| 22 |
+
status: str = "pending"
|
| 23 |
+
|
| 24 |
+
def to_dict(self) -> dict[str, Any]:
|
| 25 |
+
return asdict(self)
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def _slug(text: str) -> str:
|
| 29 |
+
s = re.sub(r"[^a-z0-9]+", "_", text.lower()).strip("_")
|
| 30 |
+
return s[:48] or "query"
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def propose_from_verdict(verdict: QueryVerdict) -> CalibrationProposal | None:
|
| 34 |
+
"""Return a reviewable proposal for non-ok verdicts."""
|
| 35 |
+
if verdict.ok or verdict.is_regression:
|
| 36 |
+
if verdict.status == "weak_core" and verdict.expected_id:
|
| 37 |
+
return CalibrationProposal(
|
| 38 |
+
proposal_id=f"alias_{_slug(verdict.query)}",
|
| 39 |
+
type="alias_patch",
|
| 40 |
+
query=verdict.query,
|
| 41 |
+
target_id=verdict.expected_id,
|
| 42 |
+
payload={"add_aliases": [verdict.query]},
|
| 43 |
+
evidence={
|
| 44 |
+
"sim": verdict.sim,
|
| 45 |
+
"tier": verdict.tier,
|
| 46 |
+
"reason": verdict.message,
|
| 47 |
+
},
|
| 48 |
+
)
|
| 49 |
+
return None
|
| 50 |
+
|
| 51 |
+
if verdict.status == "kb_gap":
|
| 52 |
+
return CalibrationProposal(
|
| 53 |
+
proposal_id=f"kb_gap_{_slug(verdict.query)}",
|
| 54 |
+
type="kb_profile_new",
|
| 55 |
+
query=verdict.query,
|
| 56 |
+
target_id=verdict.best_id,
|
| 57 |
+
payload={"query": verdict.query, "nearest_id": verdict.best_id},
|
| 58 |
+
evidence={"sim": verdict.sim, "tier": verdict.tier, "best_title": verdict.best_title},
|
| 59 |
+
)
|
| 60 |
+
|
| 61 |
+
if verdict.status == "weak_match" and verdict.best_id:
|
| 62 |
+
return CalibrationProposal(
|
| 63 |
+
proposal_id=f"alias_{_slug(verdict.query)}",
|
| 64 |
+
type="alias_patch",
|
| 65 |
+
query=verdict.query,
|
| 66 |
+
target_id=verdict.best_id,
|
| 67 |
+
payload={"add_aliases": [verdict.query]},
|
| 68 |
+
evidence={"sim": verdict.sim, "tier": verdict.tier, "best_title": verdict.best_title},
|
| 69 |
+
)
|
| 70 |
+
|
| 71 |
+
return None
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def queue_proposal(proposal: CalibrationProposal, pending_dir: str | Path) -> Path:
|
| 75 |
+
"""Write proposal JSON to pending dir (HR-5 review gate)."""
|
| 76 |
+
root = Path(pending_dir)
|
| 77 |
+
root.mkdir(parents=True, exist_ok=True)
|
| 78 |
+
path = root / f"{proposal.proposal_id}.json"
|
| 79 |
+
body = proposal.to_dict()
|
| 80 |
+
body["queued_at"] = datetime.now(timezone.utc).isoformat()
|
| 81 |
+
path.write_text(json.dumps(body, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
| 82 |
+
return path
|
services/job_query_agent/traces.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Append-only JSONL traces for query-agent runs."""
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import json
|
| 5 |
+
from datetime import datetime, timezone
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
from typing import Any
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def append_trace(path: str | Path, record: dict[str, Any]) -> None:
|
| 11 |
+
path = Path(path)
|
| 12 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 13 |
+
row = {
|
| 14 |
+
**record,
|
| 15 |
+
"ts": datetime.now(timezone.utc).isoformat(),
|
| 16 |
+
}
|
| 17 |
+
with path.open("a", encoding="utf-8") as f:
|
| 18 |
+
f.write(json.dumps(row, ensure_ascii=False) + "\n")
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def load_traces(path: str | Path, *, limit: int = 500) -> list[dict[str, Any]]:
|
| 22 |
+
path = Path(path)
|
| 23 |
+
if not path.is_file():
|
| 24 |
+
return []
|
| 25 |
+
lines = path.read_text(encoding="utf-8").splitlines()
|
| 26 |
+
out: list[dict[str, Any]] = []
|
| 27 |
+
for line in lines[-limit:]:
|
| 28 |
+
line = line.strip()
|
| 29 |
+
if line:
|
| 30 |
+
out.append(json.loads(line))
|
| 31 |
+
return out
|
tests/test_job_query_agent.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Tests for job query calibration agent (Phase 9, HR-1 offline)."""
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import json
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
|
| 7 |
+
import job_radar
|
| 8 |
+
import pytest
|
| 9 |
+
from services.job_query_agent.audit import run_audit
|
| 10 |
+
from services.job_query_agent.discover import DiscoveredQuery, discover_from_core, discover_queries
|
| 11 |
+
from services.job_query_agent.evaluate import evaluate_query
|
| 12 |
+
from services.job_query_agent.propose import propose_from_verdict
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
@pytest.fixture
|
| 16 |
+
def cfg():
|
| 17 |
+
return {
|
| 18 |
+
"job_radar": {"kb_path": "data/jobs_kb.json"},
|
| 19 |
+
"job_query_agent": {
|
| 20 |
+
"traces_path": "data/query_agent_traces_test.jsonl",
|
| 21 |
+
"discover": {
|
| 22 |
+
"include_core": True,
|
| 23 |
+
"include_seed": False,
|
| 24 |
+
"include_feedback_titles": False,
|
| 25 |
+
"include_variants": False,
|
| 26 |
+
},
|
| 27 |
+
"evaluate": {"fail_on_weak_core": True},
|
| 28 |
+
},
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def test_discover_from_core_includes_product_manager():
|
| 33 |
+
queries = {d.query for d in discover_from_core()}
|
| 34 |
+
assert "software developer" in queries
|
| 35 |
+
assert "产品经理" in queries
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def test_evaluate_core_query_ok():
|
| 39 |
+
jobs = job_radar.load_knowledge_base()
|
| 40 |
+
item = DiscoveredQuery("software developer", "core_hot", "tech_software_eng")
|
| 41 |
+
verdict = evaluate_query(item, jobs)
|
| 42 |
+
assert verdict.ok
|
| 43 |
+
assert verdict.best_id == "tech_software_eng"
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def test_evaluate_regression_detected():
|
| 47 |
+
jobs = job_radar.load_knowledge_base()
|
| 48 |
+
item = DiscoveredQuery("software developer", "core_hot", "tech_product_manager")
|
| 49 |
+
verdict = evaluate_query(item, jobs)
|
| 50 |
+
assert verdict.is_regression
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def test_propose_alias_on_weak_match():
|
| 54 |
+
jobs = job_radar.load_knowledge_base()
|
| 55 |
+
item = DiscoveredQuery("obscure made up title xyz", "seed")
|
| 56 |
+
verdict = evaluate_query(item, jobs)
|
| 57 |
+
proposal = propose_from_verdict(verdict)
|
| 58 |
+
if verdict.status == "kb_gap":
|
| 59 |
+
assert proposal is not None
|
| 60 |
+
assert proposal.type == "kb_profile_new"
|
| 61 |
+
elif verdict.status == "weak_match":
|
| 62 |
+
assert proposal is not None
|
| 63 |
+
assert proposal.type == "alias_patch"
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def test_run_audit_passes_on_real_kb(cfg, tmp_path):
|
| 67 |
+
cfg = dict(cfg)
|
| 68 |
+
cfg["job_query_agent"] = {
|
| 69 |
+
**cfg["job_query_agent"],
|
| 70 |
+
"traces_path": str(tmp_path / "traces.jsonl"),
|
| 71 |
+
}
|
| 72 |
+
summary = run_audit(cfg, write_traces=True)
|
| 73 |
+
assert summary["p0_regressions"] == 0
|
| 74 |
+
assert summary["ok"] >= len(job_radar.CORE_HOT_ROLE_QUERIES)
|
| 75 |
+
assert Path(cfg["job_query_agent"]["traces_path"]).is_file()
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def test_run_audit_fails_on_regression(cfg, tmp_path):
|
| 79 |
+
cfg = dict(cfg)
|
| 80 |
+
cfg["job_query_agent"]["traces_path"] = str(tmp_path / "traces.jsonl")
|
| 81 |
+
|
| 82 |
+
original = job_radar.CORE_HOT_ROLE_QUERIES
|
| 83 |
+
bad = (("software developer", "tech_product_manager"),) + original[1:]
|
| 84 |
+
job_radar.CORE_HOT_ROLE_QUERIES = bad
|
| 85 |
+
try:
|
| 86 |
+
with pytest.raises(AssertionError, match="P0 regression"):
|
| 87 |
+
run_audit(cfg, write_traces=False)
|
| 88 |
+
finally:
|
| 89 |
+
job_radar.CORE_HOT_ROLE_QUERIES = original
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
def test_discover_queries_respects_max(cfg):
|
| 93 |
+
cfg = dict(cfg)
|
| 94 |
+
cfg["job_query_agent"]["discover"]["max_queries_per_run"] = 3
|
| 95 |
+
items = discover_queries(cfg)
|
| 96 |
+
assert len(items) <= 3
|
tests/test_job_radar.py
CHANGED
|
@@ -123,6 +123,30 @@ def test_hybrid_query_adds_similarity():
|
|
| 123 |
# find_best_match (relevance regressions)
|
| 124 |
# --------------------------------------------------------------------------- #
|
| 125 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 126 |
def test_finance_query_matches_finance_industry():
|
| 127 |
_, best = job_radar.find_best_match("finance", _kb())
|
| 128 |
assert best is not None and best["industry"] == "Finance"
|
|
|
|
| 123 |
# find_best_match (relevance regressions)
|
| 124 |
# --------------------------------------------------------------------------- #
|
| 125 |
|
| 126 |
+
def test_core_hot_roles_match_kb():
|
| 127 |
+
"""Popular roles must resolve to curated KB entries, not LLM fallback."""
|
| 128 |
+
job_radar.assert_core_hot_role_coverage(_kb())
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
def test_software_product_manager_strong_match():
|
| 132 |
+
sim, best = job_radar.find_best_match("software product manager", _kb())
|
| 133 |
+
assert best is not None
|
| 134 |
+
assert best["id"] == "tech_product_manager"
|
| 135 |
+
assert sim >= job_radar.resolve_search_config()["tier_weak"]
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
def test_software_developer_matches_engineer():
|
| 139 |
+
sim, best = job_radar.find_best_match("software developer", _kb())
|
| 140 |
+
assert best is not None
|
| 141 |
+
assert best["id"] == "tech_software_eng"
|
| 142 |
+
assert sim >= job_radar.resolve_search_config()["tier_weak"]
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
def test_normalize_search_query_aliases():
|
| 146 |
+
assert job_radar.normalize_search_query("software developer") == "software engineer"
|
| 147 |
+
assert job_radar.normalize_search_query("Software Developer") == "software engineer"
|
| 148 |
+
|
| 149 |
+
|
| 150 |
def test_finance_query_matches_finance_industry():
|
| 151 |
_, best = job_radar.find_best_match("finance", _kb())
|
| 152 |
assert best is not None and best["industry"] == "Finance"
|
ui/i18n.py
CHANGED
|
@@ -396,6 +396,16 @@ The agent's goal is to stay well below 0.25 — the "coin-flip" baseline.""",
|
|
| 396 |
"zh": "与「{q}」无可靠匹配(相关度 {sim:.2f})。"
|
| 397 |
"将尝试 AI 生成画像,或浏览下方岗位矩阵。",
|
| 398 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 399 |
"radar_match_tier_weak": {
|
| 400 |
"en": "Weak text match for «{q}»: **{title}** (relevance {sim:.2f}). "
|
| 401 |
"Anchor role set below — career paths ranked by transition fit, not search text.",
|
|
|
|
| 396 |
"zh": "与「{q}」无可靠匹配(相关度 {sim:.2f})。"
|
| 397 |
"将尝试 AI 生成画像,或浏览下方岗位矩阵。",
|
| 398 |
},
|
| 399 |
+
"radar_llm_anchor_ok": {
|
| 400 |
+
"en": "No KB match for «{q}» (relevance {sim:.2f}). "
|
| 401 |
+
"Using AI-estimated anchor role **{title}** — opportunities below are ranked by **transition fit**, not text search.",
|
| 402 |
+
"zh": "知识库无「{q}」可靠匹配(相关度 {sim:.2f})。"
|
| 403 |
+
"已用 AI 估算锚定岗位 **{title}**——下方机会列表按**转型契合度**排序,非文本搜索。",
|
| 404 |
+
},
|
| 405 |
+
"radar_matrix_at_risk_skipped": {
|
| 406 |
+
"en": "At-risk column hidden for AI-estimated roles — KB search relevance would be misleading. See transition paths below.",
|
| 407 |
+
"zh": "AI 估算岗位不展示高风险列——知识库搜索相关度无意义。请查看下方转型路径。",
|
| 408 |
+
},
|
| 409 |
"radar_match_tier_weak": {
|
| 410 |
"en": "Weak text match for «{q}»: **{title}** (relevance {sim:.2f}). "
|
| 411 |
"Anchor role set below — career paths ranked by transition fit, not search text.",
|
ui/tabs/radar.py
CHANGED
|
@@ -39,6 +39,18 @@ def _role_status(category: str) -> str:
|
|
| 39 |
return t("status_emerging")
|
| 40 |
|
| 41 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
def render(scenario_input: dict, prior, job_radar_cfg: dict):
|
| 43 |
st.subheader(t("radar_title"))
|
| 44 |
st.markdown(t("radar_intro"))
|
|
@@ -125,6 +137,7 @@ def render(scenario_input: dict, prior, job_radar_cfg: dict):
|
|
| 125 |
)
|
| 126 |
|
| 127 |
anchor_job = None
|
|
|
|
| 128 |
llm_generated_profile = None
|
| 129 |
if search_query:
|
| 130 |
best_sim, best_job = job_radar.find_best_match(
|
|
@@ -149,14 +162,20 @@ def render(scenario_input: dict, prior, job_radar_cfg: dict):
|
|
| 149 |
llm_generated_profile["title"] += " 🤖" + t("radar_ai_badge")
|
| 150 |
all_jobs.append(llm_generated_profile)
|
| 151 |
anchor_job = llm_generated_profile
|
| 152 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 153 |
else:
|
| 154 |
st.warning(t("radar_llm_fail"))
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
elif tier == "weak":
|
| 161 |
anchor_job = best_job
|
| 162 |
st.warning(t(
|
|
@@ -185,33 +204,46 @@ def render(scenario_input: dict, prior, job_radar_cfg: dict):
|
|
| 185 |
opportunity_list = [j for j in final_jobs if j.get("category") in ("emerging", "transforming")]
|
| 186 |
|
| 187 |
# Sort lists: retrieval for at-risk; transition fit for opportunities when anchored (HR-12)
|
|
|
|
|
|
|
|
|
|
| 188 |
if search_query:
|
| 189 |
-
at_risk_list.sort(
|
| 190 |
-
key=lambda x: (x.get("combined_similarity", 0.0), x.get("hybrid_score", 0.0)),
|
| 191 |
-
reverse=True,
|
| 192 |
-
)
|
| 193 |
if anchor_job:
|
| 194 |
st.caption(t("radar_search_retrieval_note"))
|
| 195 |
st.caption(t("radar_anchor_auto", title=job_title(anchor_job)))
|
| 196 |
-
|
| 197 |
anchor_job,
|
| 198 |
-
|
| 199 |
scenario_input,
|
|
|
|
| 200 |
experience_level=user_experience,
|
| 201 |
max_retrain_months=user_retrain_cap,
|
| 202 |
job_radar_cfg=job_radar_cfg,
|
| 203 |
)
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
|
|
|
|
|
|
|
|
|
| 212 |
else:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 213 |
opportunity_list.sort(
|
| 214 |
-
key=lambda x: (
|
|
|
|
|
|
|
|
|
|
| 215 |
reverse=True,
|
| 216 |
)
|
| 217 |
else:
|
|
@@ -229,7 +261,9 @@ def render(scenario_input: dict, prior, job_radar_cfg: dict):
|
|
| 229 |
|
| 230 |
with col_risk:
|
| 231 |
st.markdown(f"#### {t('radar_at_risk')}")
|
| 232 |
-
if
|
|
|
|
|
|
|
| 233 |
st.write(t("radar_no_at_risk"))
|
| 234 |
else:
|
| 235 |
for j in at_risk_list[:5]:
|
|
|
|
| 39 |
return t("status_emerging")
|
| 40 |
|
| 41 |
|
| 42 |
+
def _merge_transition_rows(paths: list[dict], job_by_id: dict[str, dict]) -> list[dict]:
|
| 43 |
+
"""Merge compute_transition_paths output with full KB rows for matrix cards."""
|
| 44 |
+
merged: list[dict] = []
|
| 45 |
+
for p in paths:
|
| 46 |
+
base = job_by_id.get(p["id"], p)
|
| 47 |
+
row = base.copy()
|
| 48 |
+
row["transition_score"] = p.get("transition_score", 0.0)
|
| 49 |
+
row["retrain_months"] = p.get("retrain_months", row.get("retrain_months", 0))
|
| 50 |
+
merged.append(row)
|
| 51 |
+
return merged
|
| 52 |
+
|
| 53 |
+
|
| 54 |
def render(scenario_input: dict, prior, job_radar_cfg: dict):
|
| 55 |
st.subheader(t("radar_title"))
|
| 56 |
st.markdown(t("radar_intro"))
|
|
|
|
| 137 |
)
|
| 138 |
|
| 139 |
anchor_job = None
|
| 140 |
+
anchor_from_llm = False
|
| 141 |
llm_generated_profile = None
|
| 142 |
if search_query:
|
| 143 |
best_sim, best_job = job_radar.find_best_match(
|
|
|
|
| 162 |
llm_generated_profile["title"] += " 🤖" + t("radar_ai_badge")
|
| 163 |
all_jobs.append(llm_generated_profile)
|
| 164 |
anchor_job = llm_generated_profile
|
| 165 |
+
anchor_from_llm = True
|
| 166 |
+
st.info(t(
|
| 167 |
+
"radar_llm_anchor_ok",
|
| 168 |
+
title=job_title(llm_generated_profile),
|
| 169 |
+
sim=best_sim,
|
| 170 |
+
q=search_query,
|
| 171 |
+
))
|
| 172 |
else:
|
| 173 |
st.warning(t("radar_llm_fail"))
|
| 174 |
+
st.warning(t(
|
| 175 |
+
"radar_match_tier_none",
|
| 176 |
+
q=search_query,
|
| 177 |
+
sim=best_sim,
|
| 178 |
+
))
|
| 179 |
elif tier == "weak":
|
| 180 |
anchor_job = best_job
|
| 181 |
st.warning(t(
|
|
|
|
| 204 |
opportunity_list = [j for j in final_jobs if j.get("category") in ("emerging", "transforming")]
|
| 205 |
|
| 206 |
# Sort lists: retrieval for at-risk; transition fit for opportunities when anchored (HR-12)
|
| 207 |
+
job_by_id_all = {j["id"]: j for j in all_jobs}
|
| 208 |
+
for j in final_jobs:
|
| 209 |
+
job_by_id_all[j["id"]] = j
|
| 210 |
if search_query:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 211 |
if anchor_job:
|
| 212 |
st.caption(t("radar_search_retrieval_note"))
|
| 213 |
st.caption(t("radar_anchor_auto", title=job_title(anchor_job)))
|
| 214 |
+
transition_paths = job_radar.compute_transition_paths(
|
| 215 |
anchor_job,
|
| 216 |
+
all_jobs,
|
| 217 |
scenario_input,
|
| 218 |
+
top_k=5,
|
| 219 |
experience_level=user_experience,
|
| 220 |
max_retrain_months=user_retrain_cap,
|
| 221 |
job_radar_cfg=job_radar_cfg,
|
| 222 |
)
|
| 223 |
+
opportunity_list = _merge_transition_rows(transition_paths, job_by_id_all)
|
| 224 |
+
if anchor_from_llm:
|
| 225 |
+
at_risk_list = []
|
| 226 |
+
else:
|
| 227 |
+
at_risk_list.sort(
|
| 228 |
+
key=lambda x: (
|
| 229 |
+
x.get("combined_similarity", 0.0),
|
| 230 |
+
x.get("hybrid_score", 0.0),
|
| 231 |
+
),
|
| 232 |
+
reverse=True,
|
| 233 |
+
)
|
| 234 |
else:
|
| 235 |
+
at_risk_list.sort(
|
| 236 |
+
key=lambda x: (
|
| 237 |
+
x.get("combined_similarity", 0.0),
|
| 238 |
+
x.get("hybrid_score", 0.0),
|
| 239 |
+
),
|
| 240 |
+
reverse=True,
|
| 241 |
+
)
|
| 242 |
opportunity_list.sort(
|
| 243 |
+
key=lambda x: (
|
| 244 |
+
x.get("combined_similarity", 0.0),
|
| 245 |
+
x.get("hybrid_score", 0.0),
|
| 246 |
+
),
|
| 247 |
reverse=True,
|
| 248 |
)
|
| 249 |
else:
|
|
|
|
| 261 |
|
| 262 |
with col_risk:
|
| 263 |
st.markdown(f"#### {t('radar_at_risk')}")
|
| 264 |
+
if anchor_from_llm and search_query:
|
| 265 |
+
st.caption(t("radar_matrix_at_risk_skipped"))
|
| 266 |
+
elif not at_risk_list:
|
| 267 |
st.write(t("radar_no_at_risk"))
|
| 268 |
else:
|
| 269 |
for j in at_risk_list[:5]:
|