agentforge-framework 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agentforge_framework/.claude-plugin/plugin.json +4 -0
- agentforge_framework/__init__.py +3 -0
- agentforge_framework/agents/__init__.py +92 -0
- agentforge_framework/agents/architect.py +146 -0
- agentforge_framework/agents/implementer.py +162 -0
- agentforge_framework/agents/orchestrator.py +588 -0
- agentforge_framework/agents/reviewer.py +335 -0
- agentforge_framework/agents/security.py +138 -0
- agentforge_framework/agents/tester.py +125 -0
- agentforge_framework/cli.py +461 -0
- agentforge_framework/context/__init__.py +1 -0
- agentforge_framework/context/extractors/__init__.py +76 -0
- agentforge_framework/context/extractors/base.py +47 -0
- agentforge_framework/context/extractors/python.py +65 -0
- agentforge_framework/context/extractors/sql.py +121 -0
- agentforge_framework/context/extractors/yaml.py +59 -0
- agentforge_framework/context/prompt.py +104 -0
- agentforge_framework/context/resolver.py +185 -0
- agentforge_framework/core/__init__.py +1 -0
- agentforge_framework/core/commands.py +170 -0
- agentforge_framework/core/config.py +90 -0
- agentforge_framework/core/contracts.py +875 -0
- agentforge_framework/core/gates.py +333 -0
- agentforge_framework/core/issues.py +697 -0
- agentforge_framework/core/plan_format.py +272 -0
- agentforge_framework/core/process.py +141 -0
- agentforge_framework/core/project.py +262 -0
- agentforge_framework/core/registry.py +455 -0
- agentforge_framework/core/repo.py +185 -0
- agentforge_framework/core/router.py +1 -0
- agentforge_framework/core/runtime.py +639 -0
- agentforge_framework/core/skills.py +255 -0
- agentforge_framework/core/workflow.py +215 -0
- agentforge_framework/plugins/__init__.py +35 -0
- agentforge_framework/plugins/databricks/__init__.py +86 -0
- agentforge_framework/plugins/pyspark/__init__.py +57 -0
- agentforge_framework/plugins/python/__init__.py +45 -0
- agentforge_framework/plugins/sql/__init__.py +377 -0
- agentforge_framework/providers/__init__.py +48 -0
- agentforge_framework/providers/base.py +248 -0
- agentforge_framework/providers/claude.py +159 -0
- agentforge_framework/providers/codex.py +139 -0
- agentforge_framework/skills/MANIFEST.yaml +157 -0
- agentforge_framework/skills/NOTICE +49 -0
- agentforge_framework/skills/domain-modeling/ADR-FORMAT.md +47 -0
- agentforge_framework/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
- agentforge_framework/skills/domain-modeling/SKILL.md +74 -0
- agentforge_framework/skills/domain-modeling/agents/openai.yaml +3 -0
- agentforge_framework/skills/grill-with-docs/SKILL.md +76 -0
- agentforge_framework/skills/grilling/SKILL.md +28 -0
- agentforge_framework/skills/grilling/agents/openai.yaml +3 -0
- agentforge_framework/skills/to-spec/SKILL.md +75 -0
- agentforge_framework/skills/to-spec/agents/openai.yaml +5 -0
- agentforge_framework/skills/to-tickets/SKILL.md +105 -0
- agentforge_framework/skills/to-tickets/agents/openai.yaml +5 -0
- agentforge_framework/skills/unslop/SKILL.md +131 -0
- agentforge_framework/skills/unslop/evals/fixtures/silhouette/human_reference.json +66 -0
- agentforge_framework/skills/unslop/scripts/_lang.py +106 -0
- agentforge_framework/skills/unslop/scripts/banned_phrase_scan.py +784 -0
- agentforge_framework/skills/unslop/scripts/calibrate_pairs.py +580 -0
- agentforge_framework/skills/unslop/scripts/calibrate_score.py +273 -0
- agentforge_framework/skills/unslop/scripts/check_packs.py +80 -0
- agentforge_framework/skills/unslop/scripts/check_suggestions.py +225 -0
- agentforge_framework/skills/unslop/scripts/contribute.py +373 -0
- agentforge_framework/skills/unslop/scripts/diff_check.py +139 -0
- agentforge_framework/skills/unslop/scripts/extract_constraints.py +201 -0
- agentforge_framework/skills/unslop/scripts/harvest_classify.py +223 -0
- agentforge_framework/skills/unslop/scripts/harvest_samples.py +534 -0
- agentforge_framework/skills/unslop/scripts/readability_metrics.py +295 -0
- agentforge_framework/skills/unslop/scripts/refresh_status.py +154 -0
- agentforge_framework/skills/unslop/scripts/silhouette_scan.py +390 -0
- agentforge_framework/skills/unslop/scripts/structure_scan.py +322 -0
- agentforge_framework/skills/unslop/scripts/suggest.py +211 -0
- agentforge_framework/skills/unslop/scripts/validate_preservation.py +409 -0
- agentforge_framework/skills/unslop/scripts/voice_card.py +496 -0
- agentforge_framework/skills/unslop/scripts/voice_profile.py +194 -0
- agentforge_framework/skills/unslop/scripts/voice_score.py +271 -0
- agentforge_framework/skills/unslop/scripts/wiki_sync.py +479 -0
- agentforge_framework/skills/write-plainly/SKILL.md +94 -0
- agentforge_framework/workflows/bugfix.yaml +8 -0
- agentforge_framework/workflows/feature.yaml +16 -0
- agentforge_framework/workflows/review.yaml +10 -0
- agentforge_framework-0.2.0.dist-info/METADATA +321 -0
- agentforge_framework-0.2.0.dist-info/RECORD +89 -0
- agentforge_framework-0.2.0.dist-info/WHEEL +5 -0
- agentforge_framework-0.2.0.dist-info/entry_points.txt +3 -0
- agentforge_framework-0.2.0.dist-info/licenses/LICENSE +202 -0
- agentforge_framework-0.2.0.dist-info/licenses/src/agentforge_framework/skills/NOTICE +49 -0
- agentforge_framework-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Extract must-preserve constraints from input text.
|
|
4
|
+
|
|
5
|
+
Identifies facts, names, dates, URLs, numbers that must survive transformation.
|
|
6
|
+
Outputs JSON with constraint spans for validation.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
python extract_constraints.py < input.txt
|
|
10
|
+
python extract_constraints.py input.txt
|
|
11
|
+
echo "Text here" | python extract_constraints.py
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
import sys
|
|
18
|
+
import re
|
|
19
|
+
import json
|
|
20
|
+
from typing import TypedDict
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Constraint(TypedDict):
|
|
24
|
+
type: str
|
|
25
|
+
value: str
|
|
26
|
+
start: int
|
|
27
|
+
end: int
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
PATTERNS: dict[str, str] = {
|
|
31
|
+
# Currency with amounts
|
|
32
|
+
"currency": r"\$[\d,]+\.?\d*[KMBkmb]?(?:\s*(?:million|billion|thousand))?",
|
|
33
|
+
|
|
34
|
+
# Percentages
|
|
35
|
+
"percentage": r"\d+\.?\d*%",
|
|
36
|
+
|
|
37
|
+
# ISO dates
|
|
38
|
+
"date_iso": r"\d{4}-\d{2}-\d{2}",
|
|
39
|
+
|
|
40
|
+
# Quarter dates
|
|
41
|
+
"date_quarter": r"Q[1-4]\s+\d{4}",
|
|
42
|
+
|
|
43
|
+
# Natural dates
|
|
44
|
+
"date_natural": r"(?:January|February|March|April|May|June|July|August|September|October|November|December|Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)[a-z]*\.?\s+\d{1,2}(?:st|nd|rd|th)?,?\s+\d{4}",
|
|
45
|
+
|
|
46
|
+
# Years (standalone, 1900-2099)
|
|
47
|
+
"year": r"\b(?:19|20)\d{2}\b",
|
|
48
|
+
|
|
49
|
+
# Times
|
|
50
|
+
"time": r"\d{1,2}:\d{2}(?::\d{2})?\s*(?:AM|PM|am|pm|UTC|PST|EST|CST|MST|GMT)?",
|
|
51
|
+
|
|
52
|
+
# Numeric quantities with magnitude words
|
|
53
|
+
"magnitude_number": r"\b\d[\d,]*\.?\d*\s+(?:thousand|million|billion|trillion)\b",
|
|
54
|
+
|
|
55
|
+
# Measurements with units
|
|
56
|
+
"measurement": r"\d+\.?\d*\s*(?:°C|°F|degrees?\s*(?:C|F|Celsius|Fahrenheit)?|ms|s|sec|min|hr|hour|day|week|month|year|KB|MB|GB|TB|PB|kg|g|lb|oz|m|km|mi|ft|in|cm|mm|px|em|rem|%)\b",
|
|
57
|
+
|
|
58
|
+
# Phone numbers (capture the whole number, before "range" can grab a slice)
|
|
59
|
+
"phone": r"\b(?:\+?1[-.\s]?)?(?:\(\d{3}\)\s*|\d{3}[-.\s])\d{3}[-.\s]\d{4}\b",
|
|
60
|
+
|
|
61
|
+
# Ranges (numeric)
|
|
62
|
+
"range": r"\d+\.?\d*\s*[-–]\s*\d+\.?\d*(?:\s*(?:K|M|B|%|years?|months?|days?))?",
|
|
63
|
+
|
|
64
|
+
# URLs
|
|
65
|
+
"url": r"https?://[^\s\)\]\>\"\']+",
|
|
66
|
+
|
|
67
|
+
# Email addresses
|
|
68
|
+
"email": r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}",
|
|
69
|
+
|
|
70
|
+
# Code references (backticked)
|
|
71
|
+
"code": r"`[^`]+`",
|
|
72
|
+
|
|
73
|
+
# Direct quotes (longer than 10 chars)
|
|
74
|
+
"quote": r'["“][^"“”]{10,}["”]',
|
|
75
|
+
|
|
76
|
+
# Cited references (Section 12(b), Article 5, Figure 2, Table 1, Eq. 3 …) —
|
|
77
|
+
# must-preserve per fact-preservation.md and easy to silently drop.
|
|
78
|
+
"reference": r"\b(?:Sections?|Sec\.|§|Articles?|Clauses?|Paragraphs?|Para\.|Figures?|Fig\.|Tables?|Appendix|Appendices|Schedule|Exhibit|Equations?|Eq\.|Chapters?|Rules?|Items?)\s+\d+[A-Za-z]?(?:\([a-z0-9]+\))?(?:[.\-]\d+)*",
|
|
79
|
+
|
|
80
|
+
# Version numbers
|
|
81
|
+
"version": r"v?\d+\.\d+(?:\.\d+)?(?:-[a-zA-Z0-9]+)?",
|
|
82
|
+
|
|
83
|
+
# API endpoints
|
|
84
|
+
"api_endpoint": r"(?<![\w])/(?:api|v\d+)(?:/[\w-]+)+|(?<![\w])/[\w-]+(?:/[\w-]+){2,}",
|
|
85
|
+
|
|
86
|
+
# Inclusive disjunction — collapsing and/or to a plain conjunction changes scope
|
|
87
|
+
"and_or": r"\band/or\b",
|
|
88
|
+
|
|
89
|
+
# Numeric counts with context
|
|
90
|
+
"count": r"\b\d+(?:,\d{3})*\s+(?:users?|customers?|employees?|companies?|teams?|people|engineers?|developers?|items?|products?|orders?|transactions?|requests?|queries?|rows?|records?)\b",
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
# Proper noun patterns (simplified - real implementation would use NER)
|
|
94
|
+
PROPER_NOUN_INDICATORS = [
|
|
95
|
+
r"\b[A-Z][a-z]+(?:\s+[A-Z][a-z]+)+\b", # Multi-word capitalized (John Smith)
|
|
96
|
+
r"\b[A-Z][a-z]+\s+(?:Inc|Corp|LLC|Ltd|Co)\b\.?", # Company suffixes
|
|
97
|
+
r"\b(?:Dr|Mr|Ms|Mrs|Prof)\.?\s+[A-Z][a-z]+\b", # Titles with names
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def extract_constraints(text: str) -> list[Constraint]:
|
|
102
|
+
"""Extract all must-preserve constraints from text."""
|
|
103
|
+
constraints: list[Constraint] = []
|
|
104
|
+
seen_spans: set[tuple[int, int]] = set()
|
|
105
|
+
|
|
106
|
+
# Extract pattern-based constraints
|
|
107
|
+
for constraint_type, pattern in PATTERNS.items():
|
|
108
|
+
for match in re.finditer(pattern, text, re.IGNORECASE if constraint_type.startswith("date") else 0):
|
|
109
|
+
span = (match.start(), match.end())
|
|
110
|
+
if span not in seen_spans:
|
|
111
|
+
seen_spans.add(span)
|
|
112
|
+
constraints.append({
|
|
113
|
+
"type": constraint_type,
|
|
114
|
+
"value": match.group(),
|
|
115
|
+
"start": match.start(),
|
|
116
|
+
"end": match.end()
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
# Extract proper nouns (simplified)
|
|
120
|
+
for pattern in PROPER_NOUN_INDICATORS:
|
|
121
|
+
for match in re.finditer(pattern, text):
|
|
122
|
+
span = (match.start(), match.end())
|
|
123
|
+
# Don't add if overlapping with existing constraint
|
|
124
|
+
overlaps = any(
|
|
125
|
+
not (span[1] <= existing[0] or span[0] >= existing[1])
|
|
126
|
+
for existing in seen_spans
|
|
127
|
+
)
|
|
128
|
+
if not overlaps:
|
|
129
|
+
seen_spans.add(span)
|
|
130
|
+
constraints.append({
|
|
131
|
+
"type": "proper_noun",
|
|
132
|
+
"value": match.group(),
|
|
133
|
+
"start": match.start(),
|
|
134
|
+
"end": match.end()
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
# Bare quantities: comma-grouped numbers or integers of 4+ digits. These are
|
|
138
|
+
# real facts (50000 fans, 2,500 signups) that the unit/count patterns miss.
|
|
139
|
+
# Skip any that overlap a constraint already claimed (years, currency, phones)
|
|
140
|
+
# so we don't double-count or shred a captured value.
|
|
141
|
+
number_pattern = r"(?<![\d.,])(?:\d{1,3}(?:,\d{3})+|\d{4,})(?![\d.,])"
|
|
142
|
+
for match in re.finditer(number_pattern, text):
|
|
143
|
+
span = (match.start(), match.end())
|
|
144
|
+
overlaps = any(
|
|
145
|
+
not (span[1] <= existing[0] or span[0] >= existing[1])
|
|
146
|
+
for existing in seen_spans
|
|
147
|
+
)
|
|
148
|
+
if not overlaps:
|
|
149
|
+
seen_spans.add(span)
|
|
150
|
+
constraints.append({
|
|
151
|
+
"type": "number",
|
|
152
|
+
"value": match.group(),
|
|
153
|
+
"start": match.start(),
|
|
154
|
+
"end": match.end()
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
# Sort by position
|
|
158
|
+
constraints.sort(key=lambda c: c["start"])
|
|
159
|
+
|
|
160
|
+
return constraints
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def parse_args(argv: list[str]) -> argparse.Namespace:
|
|
164
|
+
parser = argparse.ArgumentParser(
|
|
165
|
+
description="Extract must-preserve constraints from input text."
|
|
166
|
+
)
|
|
167
|
+
parser.add_argument("path", nargs="?", help="Path to input text file (default: read stdin)")
|
|
168
|
+
return parser.parse_args(argv)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def main() -> None:
|
|
172
|
+
args = parse_args(sys.argv[1:])
|
|
173
|
+
|
|
174
|
+
# Read input
|
|
175
|
+
if args.path:
|
|
176
|
+
try:
|
|
177
|
+
with open(args.path, 'r', errors="replace") as f:
|
|
178
|
+
text = f.read()
|
|
179
|
+
except OSError as e:
|
|
180
|
+
print(json.dumps({"error": f"Could not read input: {e}", "constraints": []}))
|
|
181
|
+
sys.exit(2)
|
|
182
|
+
else:
|
|
183
|
+
text = sys.stdin.buffer.read().decode("utf-8", errors="replace")
|
|
184
|
+
|
|
185
|
+
if not text.strip():
|
|
186
|
+
print(json.dumps({"error": "No input provided", "constraints": []}))
|
|
187
|
+
sys.exit(1)
|
|
188
|
+
|
|
189
|
+
constraints = extract_constraints(text)
|
|
190
|
+
|
|
191
|
+
output = {
|
|
192
|
+
"input_length": len(text),
|
|
193
|
+
"constraint_count": len(constraints),
|
|
194
|
+
"constraints": constraints
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
print(json.dumps(output, indent=2))
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
if __name__ == "__main__":
|
|
201
|
+
main()
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Classify harvested candidates into situation/register coverage cells."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import json
|
|
8
|
+
import re
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
HERE = Path(__file__).resolve().parent
|
|
14
|
+
sys.path.insert(0, str(HERE))
|
|
15
|
+
|
|
16
|
+
from harvest_samples import DATE_FLOOR, recency_value # noqa: E402
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
CELLS = [
|
|
20
|
+
"numbers_data",
|
|
21
|
+
"question_addressed",
|
|
22
|
+
"anecdote_markers",
|
|
23
|
+
"disagreement",
|
|
24
|
+
"openings_closings",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def load_candidates(path: Path) -> list[dict[str, Any]]:
|
|
29
|
+
data = json.loads(path.read_text())
|
|
30
|
+
if isinstance(data, dict):
|
|
31
|
+
return data.get("candidates", [])
|
|
32
|
+
if isinstance(data, list):
|
|
33
|
+
return data
|
|
34
|
+
raise ValueError("candidates file must contain a list or {candidates: [...]}")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def cells_for(text: str) -> list[str]:
|
|
38
|
+
lowered = text.lower()
|
|
39
|
+
cells = []
|
|
40
|
+
if re.search(r"\b\d+(?:[,.]\d+)?%?\b", lowered):
|
|
41
|
+
cells.append("numbers_data")
|
|
42
|
+
if "?" in text or re.search(r"\b(?:why|how|what|when|where|which)\b", lowered):
|
|
43
|
+
cells.append("question_addressed")
|
|
44
|
+
if re.search(r"\b(?:last quarter|yesterday|once|during|when we|i noticed|i remember)\b", lowered):
|
|
45
|
+
cells.append("anecdote_markers")
|
|
46
|
+
if re.search(r"\b(?:disagree|however|instead|not convinced|push back)\b", lowered):
|
|
47
|
+
cells.append("disagreement")
|
|
48
|
+
if re.search(r"\b(?:hi|thanks|best|regards|closing|opening|first off)\b", lowered):
|
|
49
|
+
cells.append("openings_closings")
|
|
50
|
+
return cells
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def quality_for(candidate: dict[str, Any], cells: list[str]) -> int:
|
|
54
|
+
words = int(candidate.get("words") or len(re.findall(r"\w+", candidate.get("text", ""))))
|
|
55
|
+
score = 3
|
|
56
|
+
if 40 <= words <= 220:
|
|
57
|
+
score += 1
|
|
58
|
+
if cells:
|
|
59
|
+
score += 1
|
|
60
|
+
if candidate.get("suspect_ai"):
|
|
61
|
+
score -= 2
|
|
62
|
+
if candidate.get("dictated"):
|
|
63
|
+
score -= 1
|
|
64
|
+
return max(1, min(5, score))
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def candidate_id(candidate: dict[str, Any], index: int) -> Any:
|
|
68
|
+
return candidate.get("id", index)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def source_position(candidate: dict[str, Any]) -> str:
|
|
72
|
+
source = candidate.get("source", {})
|
|
73
|
+
return str(source.get("line", source.get("offset", "")))
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def coverage_from(candidates: list[dict[str, Any]]) -> dict[str, int]:
|
|
77
|
+
coverage = {cell: 0 for cell in CELLS}
|
|
78
|
+
for candidate in candidates:
|
|
79
|
+
for cell in candidate.get("cells", []):
|
|
80
|
+
coverage[cell] = coverage.get(cell, 0) + 1
|
|
81
|
+
return coverage
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def rank_enriched(candidates: list[dict[str, Any]]) -> list[int]:
|
|
85
|
+
seen_empty = set()
|
|
86
|
+
rank_rows = []
|
|
87
|
+
for idx, candidate in enumerate(candidates):
|
|
88
|
+
cells = candidate.get("cells", [])
|
|
89
|
+
fills_empty = any(cell not in seen_empty for cell in cells)
|
|
90
|
+
seen_empty.update(cells)
|
|
91
|
+
rank_rows.append((idx, fills_empty))
|
|
92
|
+
ranked = sorted(
|
|
93
|
+
rank_rows,
|
|
94
|
+
key=lambda row: (
|
|
95
|
+
not row[1],
|
|
96
|
+
-int(candidates[row[0]].get("quality") or 0),
|
|
97
|
+
-recency_value(candidates[row[0]]),
|
|
98
|
+
bool(candidates[row[0]].get("suspect_ai")),
|
|
99
|
+
bool(candidates[row[0]].get("dictated")),
|
|
100
|
+
str(candidates[row[0]].get("source", {}).get("path", "")),
|
|
101
|
+
source_position(candidates[row[0]]),
|
|
102
|
+
row[0],
|
|
103
|
+
),
|
|
104
|
+
)
|
|
105
|
+
return [candidate_id(candidates[idx], idx) for idx, _ in ranked]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def heuristic(candidates: list[dict[str, Any]]) -> dict[str, Any]:
|
|
109
|
+
enriched = []
|
|
110
|
+
seen_empty = set()
|
|
111
|
+
for idx, candidate in enumerate(candidates):
|
|
112
|
+
cells = cells_for(candidate.get("text", ""))
|
|
113
|
+
quality = quality_for(candidate, cells)
|
|
114
|
+
fills_empty = any(cell not in seen_empty for cell in cells)
|
|
115
|
+
seen_empty.update(cells)
|
|
116
|
+
enriched.append({
|
|
117
|
+
"index": idx,
|
|
118
|
+
"id": candidate.get("id", idx),
|
|
119
|
+
"cells": cells,
|
|
120
|
+
"quality": quality,
|
|
121
|
+
"why": "lexical heuristic matched " + (", ".join(cells) if cells else "no named cell"),
|
|
122
|
+
"fills_empty_coverage_cell": fills_empty,
|
|
123
|
+
"source": candidate.get("source", {}),
|
|
124
|
+
"suspect_ai": candidate.get("suspect_ai"),
|
|
125
|
+
"dictated": candidate.get("dictated"),
|
|
126
|
+
})
|
|
127
|
+
return {
|
|
128
|
+
"coverage_matrix": coverage_from(enriched),
|
|
129
|
+
"candidates": enriched,
|
|
130
|
+
"ranking": rank_enriched(enriched),
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def write_agent_tasks(candidates: list[dict[str, Any]], out_dir: Path) -> dict[str, Any]:
|
|
135
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
136
|
+
chunks = []
|
|
137
|
+
prompt = (
|
|
138
|
+
"Classify each candidate into WP10b situation/register cells. "
|
|
139
|
+
"Return JSONL rows with candidate_index, cells, quality 1-5, and one-line why. "
|
|
140
|
+
"Cells include numbers_data, question_addressed, anecdote_markers, "
|
|
141
|
+
"disagreement, openings_closings, plus any clearly justified additional cell."
|
|
142
|
+
)
|
|
143
|
+
for start in range(0, len(candidates), 10):
|
|
144
|
+
chunk = candidates[start:start + 10]
|
|
145
|
+
path = out_dir / f"harvest-classify-{start // 10 + 1:03d}.json"
|
|
146
|
+
payload = {
|
|
147
|
+
"contract": "tier-1-pack-detector",
|
|
148
|
+
"prompt": prompt,
|
|
149
|
+
"candidates": [
|
|
150
|
+
{"candidate_index": start + i, "text": c.get("text", ""), "source": c.get("source", {})}
|
|
151
|
+
for i, c in enumerate(chunk)
|
|
152
|
+
],
|
|
153
|
+
}
|
|
154
|
+
path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n")
|
|
155
|
+
chunks.append(str(path))
|
|
156
|
+
return {"task_files": chunks, "chunk_size": 10}
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def result_candidate_key(row: dict[str, Any]) -> Any:
|
|
160
|
+
for key in ("candidate_id", "id", "candidate_index", "index"):
|
|
161
|
+
if key in row:
|
|
162
|
+
return row[key]
|
|
163
|
+
raise ValueError("result row missing candidate id")
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def merge_results(candidates_path: Path, results_path: Path) -> dict[str, Any]:
|
|
167
|
+
candidates = load_candidates(candidates_path)
|
|
168
|
+
rows_by_id: dict[Any, dict[str, Any]] = {}
|
|
169
|
+
for line in results_path.read_text().splitlines():
|
|
170
|
+
if line.strip():
|
|
171
|
+
row = json.loads(line)
|
|
172
|
+
rows_by_id[result_candidate_key(row)] = row
|
|
173
|
+
|
|
174
|
+
merged = []
|
|
175
|
+
for idx, candidate in enumerate(candidates):
|
|
176
|
+
cid = candidate_id(candidate, idx)
|
|
177
|
+
row = rows_by_id.get(cid)
|
|
178
|
+
if row is None and idx in rows_by_id:
|
|
179
|
+
row = rows_by_id[idx]
|
|
180
|
+
cells = list(row.get("cells", [])) if row else []
|
|
181
|
+
quality = int(row.get("quality")) if row and row.get("quality") is not None else quality_for(candidate, cells)
|
|
182
|
+
why = str(row.get("why", "no classifier result")) if row else "no classifier result"
|
|
183
|
+
merged.append({
|
|
184
|
+
**candidate,
|
|
185
|
+
"id": cid,
|
|
186
|
+
"cells": cells,
|
|
187
|
+
"quality": max(1, min(5, quality)),
|
|
188
|
+
"why": why,
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
"coverage_matrix": coverage_from(merged),
|
|
193
|
+
"candidates": merged,
|
|
194
|
+
"ranking": rank_enriched(merged),
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def parse_args(argv: list[str]) -> argparse.Namespace:
|
|
199
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
200
|
+
parser.add_argument("--candidates", required=True)
|
|
201
|
+
parser.add_argument("--mode", choices=["heuristic", "agent"], default="heuristic")
|
|
202
|
+
parser.add_argument("--out-dir", default="harvest-agent-tasks")
|
|
203
|
+
parser.add_argument("--merge")
|
|
204
|
+
return parser.parse_args(argv)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def main(argv: list[str]) -> int:
|
|
208
|
+
args = parse_args(argv)
|
|
209
|
+
if args.merge:
|
|
210
|
+
print(json.dumps(merge_results(Path(args.candidates), Path(args.merge)), indent=2, sort_keys=True))
|
|
211
|
+
return 0
|
|
212
|
+
candidates = load_candidates(Path(args.candidates))
|
|
213
|
+
if args.mode == "heuristic":
|
|
214
|
+
print(json.dumps(heuristic(candidates), indent=2, sort_keys=True))
|
|
215
|
+
else:
|
|
216
|
+
print(json.dumps(write_agent_tasks(candidates, Path(args.out_dir)), indent=2, sort_keys=True))
|
|
217
|
+
return 0
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
if __name__ == "__main__":
|
|
221
|
+
import sys
|
|
222
|
+
|
|
223
|
+
raise SystemExit(main(sys.argv[1:]))
|