|
|
|
@@ -58,6 +58,18 @@ logger = logging.getLogger(__name__)
|
|
|
|
# Output token note (Anthropic): Opus 4.8 supports large outputs; streaming is
|
|
|
|
# Output token note (Anthropic): Opus 4.8 supports large outputs; streaming is
|
|
|
|
# handled by the CLI. `max_tokens` is advisory context for callers, not sent.
|
|
|
|
# handled by the CLI. `max_tokens` is advisory context for callers, not sent.
|
|
|
|
GENERATION_MODEL = "claude-opus-4-8" # single pinned model for every AI block (#204)
|
|
|
|
GENERATION_MODEL = "claude-opus-4-8" # single pinned model for every AI block (#204)
|
|
|
|
|
|
|
|
# 1M-context build of the SAME pinned model (#216). The default build runs a
|
|
|
|
|
|
|
|
# 200K-token context (~400K Hebrew chars at ~2 chars/token); large prompts —
|
|
|
|
|
|
|
|
# notably block-yod, which carries the full case as source-context — overflow it.
|
|
|
|
|
|
|
|
# Opus 4.8 offers a 1M-token window at standard pricing; `claude -p` exposes it
|
|
|
|
|
|
|
|
# via the `[1m]` model id directly (no beta header — verified CLI 2.1.196). We
|
|
|
|
|
|
|
|
# escalate to it only when the prompt is large, so small blocks stay on the
|
|
|
|
|
|
|
|
# cheaper/faster 200K build. NOT a parallel model (G2) — same model, wider window.
|
|
|
|
|
|
|
|
GENERATION_MODEL_1M = "claude-opus-4-8[1m]"
|
|
|
|
|
|
|
|
# Escalate to the 1M build above this prompt size. Set below the 200K-token wall
|
|
|
|
|
|
|
|
# (~400K chars) with headroom for output tokens (max_tokens up to 16K) + tokenizer
|
|
|
|
|
|
|
|
# variance, so we switch before the standard build can overflow.
|
|
|
|
|
|
|
|
_CTX_1M_THRESHOLD_CHARS = 350_000
|
|
|
|
|
|
|
|
|
|
|
|
BLOCK_CONFIG = {
|
|
|
|
BLOCK_CONFIG = {
|
|
|
|
"block-alef": {"index": 1, "title": "כותרת מוסדית", "gen_type": "template-fill", "model": "script"},
|
|
|
|
"block-alef": {"index": 1, "title": "כותרת מוסדית", "gen_type": "template-fill", "model": "script"},
|
|
|
|
@@ -461,17 +473,21 @@ async def write_block(
|
|
|
|
if not dir_doc.get("approved"):
|
|
|
|
if not dir_doc.get("approved"):
|
|
|
|
raise ValueError("לא ניתן לכתוב בלוק דיון ללא כיוון מאושר. הפעל brainstorm → approve_direction קודם.")
|
|
|
|
raise ValueError("לא ניתן לכתוב בלוק דיון ללא כיוון מאושר. הפעל brainstorm → approve_direction קודם.")
|
|
|
|
|
|
|
|
|
|
|
|
# Guard against context overflow before calling claude -p.
|
|
|
|
# Pick the model build by prompt size (#216). Above the 200K-token wall we
|
|
|
|
# Sonnet: 200K context → ~800K chars max; Opus: 200K context → same.
|
|
|
|
# escalate to the 1M-context build (`[1m]`) instead of failing the block —
|
|
|
|
# In practice the CLI has crashed on prompts above ~400K chars, so use
|
|
|
|
# block-yod legitimately carries the whole case as source-context. The 400K
|
|
|
|
# that as a conservative ceiling (well below the token limit).
|
|
|
|
# ceiling was an artifact of the old 200K-only build, NOT a model limit.
|
|
|
|
_MAX_PROMPT_CHARS = 400_000
|
|
|
|
gen_model = GENERATION_MODEL_1M if len(prompt) > _CTX_1M_THRESHOLD_CHARS else GENERATION_MODEL
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Final guard: even the 1M build is finite (~2M Hebrew chars of input). Cap at
|
|
|
|
|
|
|
|
# 1.5M chars (~750K tokens) to leave room for output + a safety margin under 1M.
|
|
|
|
|
|
|
|
_MAX_PROMPT_CHARS = 1_500_000
|
|
|
|
if len(prompt) > _MAX_PROMPT_CHARS:
|
|
|
|
if len(prompt) > _MAX_PROMPT_CHARS:
|
|
|
|
raise RuntimeError(
|
|
|
|
raise RuntimeError(
|
|
|
|
f"Prompt too large for {block_id}: {len(prompt):,} chars "
|
|
|
|
f"Prompt too large for {block_id}: {len(prompt):,} chars "
|
|
|
|
f"(limit {_MAX_PROMPT_CHARS:,}). "
|
|
|
|
f"(limit {_MAX_PROMPT_CHARS:,}, even on the 1M-context build). "
|
|
|
|
f"source_context: {len(source_context):,} chars. "
|
|
|
|
f"source_context: {len(source_context):,} chars. "
|
|
|
|
f"Reduce documents or call extract_appraiser_facts first."
|
|
|
|
f"Reduce source-context (summaries / appraiser_facts / focused RAG)."
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# Call Claude via Claude Code session (no API). #204: pin the model + per-block
|
|
|
|
# Call Claude via Claude Code session (no API). #204: pin the model + per-block
|
|
|
|
@@ -483,14 +499,14 @@ async def write_block(
|
|
|
|
content = await claude_session.query(
|
|
|
|
content = await claude_session.query(
|
|
|
|
prompt,
|
|
|
|
prompt,
|
|
|
|
timeout=timeout,
|
|
|
|
timeout=timeout,
|
|
|
|
model=GENERATION_MODEL,
|
|
|
|
model=gen_model,
|
|
|
|
effort=effort,
|
|
|
|
effort=effort,
|
|
|
|
tools="", # prose gen — no tool_use → no error_max_turns
|
|
|
|
tools="", # prose gen — no tool_use → no error_max_turns
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
sources = await _collect_block_sources(case_id, block_id)
|
|
|
|
sources = await _collect_block_sources(case_id, block_id)
|
|
|
|
sources["case_law_ids"] = _precedent_case_law_ids
|
|
|
|
sources["case_law_ids"] = _precedent_case_law_ids
|
|
|
|
result = _build_result(block_id, content, block_cfg)
|
|
|
|
result = _build_result(block_id, content, block_cfg, model_used=gen_model)
|
|
|
|
# Record the EFFECTIVE effort (override wins) so the harness can attribute
|
|
|
|
# Record the EFFECTIVE effort (override wins) so the harness can attribute
|
|
|
|
# the measured distance to the effort that actually produced the text.
|
|
|
|
# the measured distance to the effort that actually produced the text.
|
|
|
|
if result.get("effort") is not None:
|
|
|
|
if result.get("effort") is not None:
|
|
|
|
@@ -499,7 +515,8 @@ async def write_block(
|
|
|
|
return result
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _build_result(block_id: str, content: str, block_cfg: dict) -> dict:
|
|
|
|
def _build_result(block_id: str, content: str, block_cfg: dict,
|
|
|
|
|
|
|
|
model_used: str = GENERATION_MODEL) -> dict:
|
|
|
|
word_count = len(content.split())
|
|
|
|
word_count = len(content.split())
|
|
|
|
is_ai = block_cfg["model"] == "ai"
|
|
|
|
is_ai = block_cfg["model"] == "ai"
|
|
|
|
return {
|
|
|
|
return {
|
|
|
|
@@ -509,8 +526,9 @@ def _build_result(block_id: str, content: str, block_cfg: dict) -> dict:
|
|
|
|
"content": content,
|
|
|
|
"content": content,
|
|
|
|
"word_count": word_count,
|
|
|
|
"word_count": word_count,
|
|
|
|
"generation_type": block_cfg["gen_type"],
|
|
|
|
"generation_type": block_cfg["gen_type"],
|
|
|
|
# AI blocks record the pinned model; template blocks record "script".
|
|
|
|
# AI blocks record the model build actually used (200K or [1m], #216);
|
|
|
|
"model_used": GENERATION_MODEL if is_ai else block_cfg["model"],
|
|
|
|
# template blocks record "script".
|
|
|
|
|
|
|
|
"model_used": model_used if is_ai else block_cfg["model"],
|
|
|
|
# The real generation knob (#204). None for template/script blocks.
|
|
|
|
# The real generation knob (#204). None for template/script blocks.
|
|
|
|
"effort": block_cfg.get("effort", DEFAULT_EFFORT) if is_ai else None,
|
|
|
|
"effort": block_cfg.get("effort", DEFAULT_EFFORT) if is_ai else None,
|
|
|
|
# DEPRECATED: temperature is not a real knob on Opus 4.7/4.8 (sending it
|
|
|
|
# DEPRECATED: temperature is not a real knob on Opus 4.7/4.8 (sending it
|
|
|
|
|