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("--max-calls", type=int, default=None)
|
||||||
generate_plan.add_argument("--cost-cap", type=float, default=None)
|
generate_plan.add_argument("--cost-cap", type=float, default=None)
|
||||||
generate_plan.add_argument(
|
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(
|
generate_plan.add_argument(
|
||||||
"--entities-per-chunk", type=int, default=2, help="Estimate of entities each chunk yields"
|
"--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,
|
max_calls=args.max_calls,
|
||||||
cost_cap=args.cost_cap,
|
cost_cap=args.cost_cap,
|
||||||
cost_per_1k_tokens=args.cost_per_1k,
|
cost_per_1k_tokens=args.cost_per_1k,
|
||||||
|
model=args.model or None,
|
||||||
entities_per_chunk=args.entities_per_chunk,
|
entities_per_chunk=args.entities_per_chunk,
|
||||||
full=args.full,
|
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]:
|
def _read_frontmatter_markdown(path: Path) -> tuple[dict[str, Any], str]:
|
||||||
text = path.read_text(encoding="utf-8")
|
text = path.read_text(encoding="utf-8")
|
||||||
if not text.startswith(f"{FRONTMATTER_MARKER}\n"):
|
raw, body = _extract_frontmatter_block(text, path)
|
||||||
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 :]
|
|
||||||
data = yaml.safe_load(raw)
|
data = yaml.safe_load(raw)
|
||||||
if not isinstance(data, dict):
|
if not isinstance(data, dict):
|
||||||
raise InfospaceError(
|
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}",
|
f"Expected mapping frontmatter in evaluation file: {path}",
|
||||||
{"path": str(path)},
|
{"path": str(path)},
|
||||||
)
|
)
|
||||||
|
_normalise_scores(data)
|
||||||
return data, body
|
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]:
|
def _parse_rationales(body: str) -> dict[str, str]:
|
||||||
rationales: dict[str, str] = {}
|
rationales: dict[str, str] = {}
|
||||||
current_name: str | None = None
|
current_name: str | None = None
|
||||||
|
|||||||
@@ -144,6 +144,7 @@ def plan_generation(
|
|||||||
max_calls: int | None = None,
|
max_calls: int | None = None,
|
||||||
cost_cap: float | None = None,
|
cost_cap: float | None = None,
|
||||||
cost_per_1k_tokens: float = 0.0,
|
cost_per_1k_tokens: float = 0.0,
|
||||||
|
model: str | None = None,
|
||||||
words_per_token: float = WORDS_PER_TOKEN_DEFAULT,
|
words_per_token: float = WORDS_PER_TOKEN_DEFAULT,
|
||||||
entities_per_chunk: int = ENTITIES_PER_CHUNK_ESTIMATE,
|
entities_per_chunk: int = ENTITIES_PER_CHUNK_ESTIMATE,
|
||||||
full: bool = False,
|
full: bool = False,
|
||||||
@@ -161,6 +162,7 @@ def plan_generation(
|
|||||||
max_calls=max_calls,
|
max_calls=max_calls,
|
||||||
cost_cap=cost_cap,
|
cost_cap=cost_cap,
|
||||||
cost_per_1k_tokens=cost_per_1k_tokens,
|
cost_per_1k_tokens=cost_per_1k_tokens,
|
||||||
|
model=model,
|
||||||
words_per_token=words_per_token,
|
words_per_token=words_per_token,
|
||||||
entities_per_chunk=entities_per_chunk,
|
entities_per_chunk=entities_per_chunk,
|
||||||
)
|
)
|
||||||
@@ -203,6 +205,7 @@ def plan_generation_summary(
|
|||||||
max_calls: int | None = None,
|
max_calls: int | None = None,
|
||||||
cost_cap: float | None = None,
|
cost_cap: float | None = None,
|
||||||
cost_per_1k_tokens: float = 0.0,
|
cost_per_1k_tokens: float = 0.0,
|
||||||
|
model: str | None = None,
|
||||||
words_per_token: float = WORDS_PER_TOKEN_DEFAULT,
|
words_per_token: float = WORDS_PER_TOKEN_DEFAULT,
|
||||||
entities_per_chunk: int = ENTITIES_PER_CHUNK_ESTIMATE,
|
entities_per_chunk: int = ENTITIES_PER_CHUNK_ESTIMATE,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
@@ -247,9 +250,29 @@ def plan_generation_summary(
|
|||||||
total_calls += calls
|
total_calls += calls
|
||||||
total_prompt_words += prompt_words
|
total_prompt_words += prompt_words
|
||||||
total_tokens = int(round(total_prompt_words / words_per_token)) if words_per_token > 0 else 0
|
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
|
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 = round((total_tokens / 1000.0) * cost_per_1k_tokens, 4)
|
||||||
|
cost_source = "cost_per_1k_blended"
|
||||||
chapter_numbers = sorted(
|
chapter_numbers = sorted(
|
||||||
{
|
{
|
||||||
int(item.provenance.get("chapter_number"))
|
int(item.provenance.get("chapter_number"))
|
||||||
@@ -267,7 +290,10 @@ def plan_generation_summary(
|
|||||||
"total_provider_calls_estimate": total_calls,
|
"total_provider_calls_estimate": total_calls,
|
||||||
"total_prompt_words_estimate": total_prompt_words,
|
"total_prompt_words_estimate": total_prompt_words,
|
||||||
"total_prompt_tokens_estimate": total_tokens,
|
"total_prompt_tokens_estimate": total_tokens,
|
||||||
|
"estimated_completion_tokens": estimated_completion_tokens,
|
||||||
"estimated_cost_usd": cost,
|
"estimated_cost_usd": cost,
|
||||||
|
"cost_source": cost_source,
|
||||||
|
"model": model,
|
||||||
"cost_per_1k_tokens": cost_per_1k_tokens or None,
|
"cost_per_1k_tokens": cost_per_1k_tokens or None,
|
||||||
"words_per_token": words_per_token,
|
"words_per_token": words_per_token,
|
||||||
"entities_per_chunk_estimate": entities_per_chunk,
|
"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:
|
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)
|
raw = Path(path)
|
||||||
target = raw if raw.is_absolute() else root / raw
|
|
||||||
root_resolved = root.resolve()
|
root_resolved = root.resolve()
|
||||||
target_resolved = target.resolve()
|
if raw.is_absolute():
|
||||||
try:
|
candidates = [raw.resolve()]
|
||||||
return str(target_resolved.relative_to(root_resolved))
|
else:
|
||||||
except ValueError as exc:
|
cwd_candidate = raw.resolve()
|
||||||
raise InfospaceError(
|
joined_candidate = (root / raw).resolve()
|
||||||
"artifact_path_escapes_infospace",
|
candidates = [cwd_candidate]
|
||||||
f"Artifact path escapes infospace root: {path}",
|
if joined_candidate != cwd_candidate:
|
||||||
{"root": str(root), "path": str(path)},
|
candidates.append(joined_candidate)
|
||||||
) from exc
|
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:
|
def _write_yaml(path: Path, data: dict[str, Any]) -> None:
|
||||||
|
|||||||
@@ -3,9 +3,11 @@
|
|||||||
Profile: {{ macros.profile }}
|
Profile: {{ macros.profile }}
|
||||||
|
|
||||||
Extract reusable infospace entities from the source chunk. Return one Markdown
|
Extract reusable infospace entities from the source chunk. Return one Markdown
|
||||||
bundle where each entity starts with `# Entity Title` and contains at least a
|
bundle where each entity starts with a level-1 heading that is the entity's
|
||||||
`## Definition` section. Prefer durable concepts, claims, named methods,
|
own name (e.g. `# Knowledge Artifact`, `# Source Claim` — **not** the literal
|
||||||
people, places, works, and objects over sentence fragments.
|
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 title: {{ input.title }}
|
||||||
Source artifact: {{ input.artifact_id }}
|
Source artifact: {{ input.artifact_id }}
|
||||||
|
|||||||
@@ -3,8 +3,10 @@
|
|||||||
Profile: {{ macros.profile }}
|
Profile: {{ macros.profile }}
|
||||||
|
|
||||||
Extract reusable infospace entities from the source chunk. Return one
|
Extract reusable infospace entities from the source chunk. Return one
|
||||||
Markdown bundle where each entity starts with `# Entity Title` and has a
|
Markdown bundle where each entity starts with a level-1 heading that is
|
||||||
`## Definition` section, plus a `## Category` line drawn from the list
|
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
|
below. Add `## Context` and `## Source Evidence` when the chunk gives
|
||||||
enough material; leave them out rather than inventing detail.
|
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
|
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:
|
def test_plan_full_mode_includes_workflow_plans(tmp_path: Path) -> None:
|
||||||
root = _build_plan_infospace(tmp_path)
|
root = _build_plan_infospace(tmp_path)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user