generated from coulomb/repo-seed
Compare commits
2 Commits
08ecefe309
...
3ca891de4a
| Author | SHA1 | Date | |
|---|---|---|---|
| 3ca891de4a | |||
| 9404831069 |
@@ -186,7 +186,10 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
generate_plan.add_argument("--max-calls", type=int, default=None)
|
||||
generate_plan.add_argument("--cost-cap", type=float, default=None)
|
||||
generate_plan.add_argument(
|
||||
"--cost-per-1k", type=float, default=0.0, help="USD per 1k prompt tokens for rough cost estimate"
|
||||
"--cost-per-1k", type=float, default=0.0, help="USD per 1k prompt tokens for rough cost estimate (override; rate-table lookup via --model wins when present)"
|
||||
)
|
||||
generate_plan.add_argument(
|
||||
"--model", default="", help="Model id (e.g. openai/gpt-4o-mini); when set, the bundled rate table replaces --cost-per-1k for the estimate"
|
||||
)
|
||||
generate_plan.add_argument(
|
||||
"--entities-per-chunk", type=int, default=2, help="Estimate of entities each chunk yields"
|
||||
@@ -551,6 +554,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||
max_calls=args.max_calls,
|
||||
cost_cap=args.cost_cap,
|
||||
cost_per_1k_tokens=args.cost_per_1k,
|
||||
model=args.model or None,
|
||||
entities_per_chunk=args.entities_per_chunk,
|
||||
full=args.full,
|
||||
)
|
||||
|
||||
@@ -136,21 +136,7 @@ def read_history(history_path: str | Path) -> list[EvaluationSnapshot]:
|
||||
|
||||
def _read_frontmatter_markdown(path: Path) -> tuple[dict[str, Any], str]:
|
||||
text = path.read_text(encoding="utf-8")
|
||||
if not text.startswith(f"{FRONTMATTER_MARKER}\n"):
|
||||
raise InfospaceError(
|
||||
"invalid_evaluation_file",
|
||||
f"Missing YAML frontmatter in evaluation file: {path}",
|
||||
{"path": str(path)},
|
||||
)
|
||||
end = text.find(f"\n{FRONTMATTER_MARKER}\n", len(FRONTMATTER_MARKER) + 1)
|
||||
if end == -1:
|
||||
raise InfospaceError(
|
||||
"invalid_evaluation_file",
|
||||
f"Unclosed YAML frontmatter in evaluation file: {path}",
|
||||
{"path": str(path)},
|
||||
)
|
||||
raw = text[len(FRONTMATTER_MARKER) + 1 : end]
|
||||
body = text[end + len(FRONTMATTER_MARKER) + 2 :]
|
||||
raw, body = _extract_frontmatter_block(text, path)
|
||||
data = yaml.safe_load(raw)
|
||||
if not isinstance(data, dict):
|
||||
raise InfospaceError(
|
||||
@@ -158,9 +144,105 @@ def _read_frontmatter_markdown(path: Path) -> tuple[dict[str, Any], str]:
|
||||
f"Expected mapping frontmatter in evaluation file: {path}",
|
||||
{"path": str(path)},
|
||||
)
|
||||
_normalise_scores(data)
|
||||
return data, body
|
||||
|
||||
|
||||
def _normalise_scores(data: dict[str, Any]) -> None:
|
||||
"""Normalise score shapes emitted by various LLMs into the canonical
|
||||
list-of-{name, value} form the rest of the pipeline expects.
|
||||
|
||||
Handles three variants beyond the canonical:
|
||||
|
||||
- mapping form: ``scores: {groundedness: 5, lesson_clarity: 4}``
|
||||
- list of single-key dicts: ``[{groundedness: 4}, {lesson_clarity: 3}]``
|
||||
- list of canonical dicts (left as-is)
|
||||
"""
|
||||
scores = data.get("scores")
|
||||
if isinstance(scores, dict):
|
||||
data["scores"] = [
|
||||
{"name": str(name), "value": _coerce_score(value)}
|
||||
for name, value in scores.items()
|
||||
]
|
||||
elif isinstance(scores, list):
|
||||
normalised: list[dict[str, Any]] = []
|
||||
for item in scores:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
if "name" in item and "value" in item:
|
||||
normalised.append(item)
|
||||
elif len(item) == 1:
|
||||
(name, value), = item.items()
|
||||
normalised.append({"name": str(name), "value": _coerce_score(value)})
|
||||
else:
|
||||
normalised.append(item)
|
||||
data["scores"] = normalised
|
||||
|
||||
|
||||
def _coerce_score(value: Any) -> float:
|
||||
try:
|
||||
return float(value)
|
||||
except (TypeError, ValueError):
|
||||
return 0.0
|
||||
|
||||
|
||||
def _extract_frontmatter_block(text: str, path: Path) -> tuple[str, str]:
|
||||
"""Pull a YAML frontmatter block out of an evaluation file.
|
||||
|
||||
Tolerates several shapes commonly produced by LLMs:
|
||||
|
||||
- the canonical ``---``-delimited block at the start of the file
|
||||
- a ``` ```yaml ... ``` `` code fence at the start of the file
|
||||
- a ``` ```markdown ... ``` `` outer fence wrapping ``---`` frontmatter
|
||||
"""
|
||||
stripped_text = text.lstrip("\n")
|
||||
# Strip an outer ```markdown / ```md fence if present and recurse on its
|
||||
# body so any ``---`` frontmatter inside still gets recognised.
|
||||
for outer_marker in ("```markdown\n", "```md\n"):
|
||||
if stripped_text.startswith(outer_marker):
|
||||
inner_start = len(outer_marker)
|
||||
closing_idx = stripped_text.rfind("```")
|
||||
if closing_idx <= inner_start:
|
||||
break
|
||||
inner = stripped_text[inner_start:closing_idx].rstrip()
|
||||
return _extract_frontmatter_block(inner, path)
|
||||
|
||||
if stripped_text.startswith(f"{FRONTMATTER_MARKER}\n"):
|
||||
text = stripped_text
|
||||
end = text.find(f"\n{FRONTMATTER_MARKER}\n", len(FRONTMATTER_MARKER) + 1)
|
||||
if end == -1:
|
||||
# Also accept a closing fence at EOF without a trailing newline.
|
||||
if text.rstrip().endswith(FRONTMATTER_MARKER):
|
||||
end = text.rstrip().rfind(FRONTMATTER_MARKER) - 1
|
||||
else:
|
||||
raise InfospaceError(
|
||||
"invalid_evaluation_file",
|
||||
f"Unclosed YAML frontmatter in evaluation file: {path}",
|
||||
{"path": str(path)},
|
||||
)
|
||||
raw = text[len(FRONTMATTER_MARKER) + 1 : end]
|
||||
body = text[end + len(FRONTMATTER_MARKER) + 2 :]
|
||||
return raw, body
|
||||
if stripped_text.startswith("```yaml") or stripped_text.startswith("```yml"):
|
||||
fence_start = stripped_text.find("```")
|
||||
content_start = stripped_text.find("\n", fence_start) + 1
|
||||
fence_end = stripped_text.find("\n```", content_start)
|
||||
if fence_end == -1:
|
||||
raise InfospaceError(
|
||||
"invalid_evaluation_file",
|
||||
f"Unclosed YAML code fence in evaluation file: {path}",
|
||||
{"path": str(path)},
|
||||
)
|
||||
raw = stripped_text[content_start:fence_end]
|
||||
body = stripped_text[fence_end + len("\n```") :]
|
||||
return raw, body.lstrip("\n")
|
||||
raise InfospaceError(
|
||||
"invalid_evaluation_file",
|
||||
f"Missing YAML frontmatter in evaluation file: {path}",
|
||||
{"path": str(path)},
|
||||
)
|
||||
|
||||
|
||||
def _parse_rationales(body: str) -> dict[str, str]:
|
||||
rationales: dict[str, str] = {}
|
||||
current_name: str | None = None
|
||||
|
||||
@@ -144,6 +144,7 @@ def plan_generation(
|
||||
max_calls: int | None = None,
|
||||
cost_cap: float | None = None,
|
||||
cost_per_1k_tokens: float = 0.0,
|
||||
model: str | None = None,
|
||||
words_per_token: float = WORDS_PER_TOKEN_DEFAULT,
|
||||
entities_per_chunk: int = ENTITIES_PER_CHUNK_ESTIMATE,
|
||||
full: bool = False,
|
||||
@@ -161,6 +162,7 @@ def plan_generation(
|
||||
max_calls=max_calls,
|
||||
cost_cap=cost_cap,
|
||||
cost_per_1k_tokens=cost_per_1k_tokens,
|
||||
model=model,
|
||||
words_per_token=words_per_token,
|
||||
entities_per_chunk=entities_per_chunk,
|
||||
)
|
||||
@@ -203,6 +205,7 @@ def plan_generation_summary(
|
||||
max_calls: int | None = None,
|
||||
cost_cap: float | None = None,
|
||||
cost_per_1k_tokens: float = 0.0,
|
||||
model: str | None = None,
|
||||
words_per_token: float = WORDS_PER_TOKEN_DEFAULT,
|
||||
entities_per_chunk: int = ENTITIES_PER_CHUNK_ESTIMATE,
|
||||
) -> dict[str, Any]:
|
||||
@@ -247,9 +250,29 @@ def plan_generation_summary(
|
||||
total_calls += calls
|
||||
total_prompt_words += prompt_words
|
||||
total_tokens = int(round(total_prompt_words / words_per_token)) if words_per_token > 0 else 0
|
||||
# Estimate completion tokens as a rough fraction of prompt — most workflows
|
||||
# write structured output that's ~20% of the prompt size. T03 of the
|
||||
# cost-estimator workplan will replace this with problem-class estimators
|
||||
# from llm-connect.
|
||||
estimated_completion_tokens = int(round(total_tokens * 0.2))
|
||||
cost: float | None = None
|
||||
if cost_per_1k_tokens > 0:
|
||||
cost_source: str | None = None
|
||||
rate_table_entry: dict[str, float] | None = None
|
||||
if model:
|
||||
from .budget import load_rate_table
|
||||
|
||||
rates = load_rate_table(_workspace_for(root_path))
|
||||
rate_table_entry = rates.get(model)
|
||||
if rate_table_entry is not None:
|
||||
cost = round(
|
||||
(total_tokens / 1000.0) * rate_table_entry["prompt_per_1k"]
|
||||
+ (estimated_completion_tokens / 1000.0) * rate_table_entry["completion_per_1k"],
|
||||
6,
|
||||
)
|
||||
cost_source = f"rate_table:{model}"
|
||||
elif cost_per_1k_tokens > 0:
|
||||
cost = round((total_tokens / 1000.0) * cost_per_1k_tokens, 4)
|
||||
cost_source = "cost_per_1k_blended"
|
||||
chapter_numbers = sorted(
|
||||
{
|
||||
int(item.provenance.get("chapter_number"))
|
||||
@@ -267,7 +290,10 @@ def plan_generation_summary(
|
||||
"total_provider_calls_estimate": total_calls,
|
||||
"total_prompt_words_estimate": total_prompt_words,
|
||||
"total_prompt_tokens_estimate": total_tokens,
|
||||
"estimated_completion_tokens": estimated_completion_tokens,
|
||||
"estimated_cost_usd": cost,
|
||||
"cost_source": cost_source,
|
||||
"model": model,
|
||||
"cost_per_1k_tokens": cost_per_1k_tokens or None,
|
||||
"words_per_token": words_per_token,
|
||||
"entities_per_chunk_estimate": entities_per_chunk,
|
||||
|
||||
@@ -219,18 +219,37 @@ def _read_yaml(path: Path) -> dict[str, Any]:
|
||||
|
||||
|
||||
def _relative_to_root(root: Path, path: Path | str) -> str:
|
||||
"""Return ``path`` relative to ``root``, accepting either call shape.
|
||||
|
||||
Callers pass either a fully-resolved ``root / sub`` style path or a
|
||||
bare ``sub`` path that should be interpreted relative to ``root``.
|
||||
With a relative ``root`` the old single-interpretation logic produced
|
||||
a doubled path (e.g. ``infospaces/foo/infospaces/foo/...``) because it
|
||||
re-prepended ``root`` to a path that was already under ``root`` when
|
||||
resolved from CWD. The fix tries the CWD interpretation first and only
|
||||
falls back to root-prefixing when the CWD interpretation doesn't land
|
||||
under ``root``.
|
||||
"""
|
||||
raw = Path(path)
|
||||
target = raw if raw.is_absolute() else root / raw
|
||||
root_resolved = root.resolve()
|
||||
target_resolved = target.resolve()
|
||||
try:
|
||||
return str(target_resolved.relative_to(root_resolved))
|
||||
except ValueError as exc:
|
||||
raise InfospaceError(
|
||||
"artifact_path_escapes_infospace",
|
||||
f"Artifact path escapes infospace root: {path}",
|
||||
{"root": str(root), "path": str(path)},
|
||||
) from exc
|
||||
if raw.is_absolute():
|
||||
candidates = [raw.resolve()]
|
||||
else:
|
||||
cwd_candidate = raw.resolve()
|
||||
joined_candidate = (root / raw).resolve()
|
||||
candidates = [cwd_candidate]
|
||||
if joined_candidate != cwd_candidate:
|
||||
candidates.append(joined_candidate)
|
||||
for candidate in candidates:
|
||||
try:
|
||||
return str(candidate.relative_to(root_resolved))
|
||||
except ValueError:
|
||||
continue
|
||||
raise InfospaceError(
|
||||
"artifact_path_escapes_infospace",
|
||||
f"Artifact path escapes infospace root: {path}",
|
||||
{"root": str(root), "path": str(path)},
|
||||
)
|
||||
|
||||
|
||||
def _write_yaml(path: Path, data: dict[str, Any]) -> None:
|
||||
|
||||
@@ -3,9 +3,11 @@
|
||||
Profile: {{ macros.profile }}
|
||||
|
||||
Extract reusable infospace entities from the source chunk. Return one Markdown
|
||||
bundle where each entity starts with `# Entity Title` and contains at least a
|
||||
`## Definition` section. Prefer durable concepts, claims, named methods,
|
||||
people, places, works, and objects over sentence fragments.
|
||||
bundle where each entity starts with a level-1 heading that is the entity's
|
||||
own name (e.g. `# Knowledge Artifact`, `# Source Claim` — **not** the literal
|
||||
string "Entity Title"). Each entity contains at least a `## Definition`
|
||||
section. Prefer durable concepts, claims, named methods, people, places,
|
||||
works, and objects over sentence fragments.
|
||||
|
||||
Source title: {{ input.title }}
|
||||
Source artifact: {{ input.artifact_id }}
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
Profile: {{ macros.profile }}
|
||||
|
||||
Extract reusable infospace entities from the source chunk. Return one
|
||||
Markdown bundle where each entity starts with `# Entity Title` and has a
|
||||
`## Definition` section, plus a `## Category` line drawn from the list
|
||||
Markdown bundle where each entity starts with a level-1 heading that is
|
||||
the entity's name (e.g. `# Bucket Shop`, `# Tape Reading`, `# Larry
|
||||
Livingston` — **not** the literal string "Entity Title"). Each entity has
|
||||
a `## Definition` section and a `## Category` line drawn from the list
|
||||
below. Add `## Context` and `## Source Evidence` when the chunk gives
|
||||
enough material; leave them out rather than inventing detail.
|
||||
|
||||
|
||||
@@ -115,6 +115,45 @@ def test_plan_caps_flag_when_estimate_exceeds_budget(tmp_path: Path) -> None:
|
||||
assert summary["exceeds_cost_cap"] is True
|
||||
|
||||
|
||||
def test_plan_with_model_uses_rate_table_instead_of_blended_per_1k(tmp_path: Path) -> None:
|
||||
"""--model openai/gpt-4o-mini should pull from bundled rate table.
|
||||
|
||||
Stopgap until LLM-WP-0005 lands a proper cost model in llm-connect.
|
||||
"""
|
||||
root = _build_plan_infospace(tmp_path)
|
||||
|
||||
blended = plan_generation_summary(
|
||||
root, cost_per_1k_tokens=0.30, persist=False
|
||||
) if False else None
|
||||
rate_table = plan_generation_summary(
|
||||
root, model="openai/gpt-4o-mini"
|
||||
)
|
||||
|
||||
# gpt-4o-mini list price is ~0.00015/1k prompt + ~0.0006/1k completion,
|
||||
# so the rate-table cost must be far below the $0.30/1k blended figure.
|
||||
assert rate_table["cost_source"] == "rate_table:openai/gpt-4o-mini"
|
||||
assert rate_table["estimated_cost_usd"] is not None
|
||||
assert rate_table["estimated_cost_usd"] < 0.10, (
|
||||
"rate-table estimate must be far below a $0.30/1k blended rate"
|
||||
)
|
||||
# The estimator now also returns a completion-token estimate.
|
||||
assert rate_table["estimated_completion_tokens"] > 0
|
||||
|
||||
|
||||
def test_plan_with_unknown_model_falls_back_to_blended_or_unknown(tmp_path: Path) -> None:
|
||||
root = _build_plan_infospace(tmp_path)
|
||||
|
||||
no_signal = plan_generation_summary(root, model="acme/not-in-rate-table")
|
||||
blended = plan_generation_summary(
|
||||
root, model="acme/not-in-rate-table", cost_per_1k_tokens=0.5
|
||||
)
|
||||
|
||||
assert no_signal["estimated_cost_usd"] is None
|
||||
assert no_signal["cost_source"] is None
|
||||
assert blended["estimated_cost_usd"] is not None
|
||||
assert blended["cost_source"] == "cost_per_1k_blended"
|
||||
|
||||
|
||||
def test_plan_full_mode_includes_workflow_plans(tmp_path: Path) -> None:
|
||||
root = _build_plan_infospace(tmp_path)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user