fix(planning): the plan gate joins step text through embedding_text (#4079)
test_nothing_else_builds_the_embedding_document_itself caught start_planning
building f"{title}\n{body}" inline for the plan gate's candidate text. That is
the embedded-document shape; plan_candidate_text now takes (title, body) pairs
and calls embedding_text, so the candidate moves with the corpus it is ranked
against (#2486).
Also carries the create_task / create_records milestone_id docstring lines from
step 5 (#4080), which share the file.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01821k5B3Ysecp9fNYs92Kuy
This commit is contained in:
@@ -152,6 +152,9 @@ async def create_task(
|
|||||||
priority: One of: low, medium, high, or 'none'. Omit (empty string) to leave unset.
|
priority: One of: low, medium, high, or 'none'. Omit (empty string) to leave unset.
|
||||||
project_id: Associate with a project (0 = no project).
|
project_id: Associate with a project (0 = no project).
|
||||||
milestone_id: Place within a project milestone (0 = no milestone).
|
milestone_id: Place within a project milestone (0 = no milestone).
|
||||||
|
When the work belongs to an active plan — a milestone in
|
||||||
|
enter_project's lists or found by search(content_type=
|
||||||
|
"milestone") — pass its id, so the plan shows all of its work.
|
||||||
parent_id: Make this a sub-task of another task (0 = top-level).
|
parent_id: Make this a sub-task of another task (0 = top-level).
|
||||||
tags: List of plain-string tags without # prefix.
|
tags: List of plain-string tags without # prefix.
|
||||||
kind: 'work' (default), 'issue', or 'spike'.
|
kind: 'work' (default), 'issue', or 'spike'.
|
||||||
@@ -462,6 +465,8 @@ async def create_records(
|
|||||||
project_id: The project every record belongs to (0 = none, or taken
|
project_id: The project every record belongs to (0 = none, or taken
|
||||||
from milestone_id).
|
from milestone_id).
|
||||||
milestone_id: File every record under this existing milestone (0 = none).
|
milestone_id: File every record under this existing milestone (0 = none).
|
||||||
|
This is how steps are added to a plan that already exists,
|
||||||
|
including one start_planning handed back as `existing_milestone`.
|
||||||
force: Bypass the near-duplicate gate for the whole batch. By default
|
force: Bypass the near-duplicate gate for the whole batch. By default
|
||||||
the first record that near-duplicates an existing one BLOCKS the
|
the first record that near-duplicates an existing one BLOCKS the
|
||||||
batch, and its existing id comes back so you can update it instead.
|
batch, and its existing id comes back so you can update it instead.
|
||||||
@@ -553,7 +558,7 @@ async def start_planning(
|
|||||||
match = await dedup_svc.plan_gate(
|
match = await dedup_svc.plan_gate(
|
||||||
uid, project_id, title,
|
uid, project_id, title,
|
||||||
dedup_svc.plan_candidate_text(
|
dedup_svc.plan_candidate_text(
|
||||||
body=body, step_texts=[f"{i.title}\n{i.body or ''}" for i in items],
|
body=body, steps=[(i.title, i.body) for i in items],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
if match is not None:
|
if match is not None:
|
||||||
|
|||||||
@@ -767,16 +767,17 @@ async def get_plan_match_threshold(user_id: int) -> float:
|
|||||||
def plan_candidate_text(
|
def plan_candidate_text(
|
||||||
description: str | None = None,
|
description: str | None = None,
|
||||||
body: str | None = None,
|
body: str | None = None,
|
||||||
step_texts: list[str] | None = None,
|
steps: list[tuple[str | None, str | None]] | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""What a plan that doesn't exist yet says about itself, for the gate.
|
"""What a plan that doesn't exist yet says about itself, for the gate.
|
||||||
|
|
||||||
The steps belong in it: a plan passed with steps and no design is still
|
The steps belong in it: a plan passed with steps and no design is still
|
||||||
recognisable by them, and what its steps say is most of what makes two
|
recognisable by them, and what its steps say is most of what makes two
|
||||||
plans the same plan.
|
plans the same plan. Each step is (title, body), joined by embedding_text
|
||||||
|
like every other record that becomes embedded text (#2486).
|
||||||
"""
|
"""
|
||||||
parts = [(description or "").strip(), (body or "").strip()]
|
parts = [(description or "").strip(), (body or "").strip()]
|
||||||
parts += [t.strip() for t in (step_texts or [])]
|
parts += [embeddings_svc.embedding_text(t, b) for t, b in (steps or [])]
|
||||||
return "\n\n".join(p for p in parts if p)
|
return "\n\n".join(p for p in parts if p)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -408,8 +408,9 @@ async def test_a_bad_threshold_setting_falls_back_to_the_default():
|
|||||||
|
|
||||||
|
|
||||||
def test_plan_candidate_text_carries_the_steps():
|
def test_plan_candidate_text_carries_the_steps():
|
||||||
text = plan_candidate_text(description=None, body=" design ", step_texts=["Step one\n", ""])
|
text = plan_candidate_text(description=None, body=" design ",
|
||||||
assert text == "design\n\nStep one"
|
steps=[("Step one", None), ("Step two", "with a body"), (None, None)])
|
||||||
|
assert text == "design\n\nStep one\n\nStep two\nwith a body"
|
||||||
|
|
||||||
|
|
||||||
def test_plan_match_response_points_at_adding_steps_not_a_second_plan():
|
def test_plan_match_response_points_at_adding_steps_not_a_second_plan():
|
||||||
|
|||||||
Reference in New Issue
Block a user