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.
|
||||
project_id: Associate with a project (0 = no project).
|
||||
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).
|
||||
tags: List of plain-string tags without # prefix.
|
||||
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
|
||||
from milestone_id).
|
||||
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
|
||||
the first record that near-duplicates an existing one BLOCKS the
|
||||
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(
|
||||
uid, project_id, title,
|
||||
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:
|
||||
|
||||
@@ -767,16 +767,17 @@ async def get_plan_match_threshold(user_id: int) -> float:
|
||||
def plan_candidate_text(
|
||||
description: str | None = None,
|
||||
body: str | None = None,
|
||||
step_texts: list[str] | None = None,
|
||||
steps: list[tuple[str | None, str | None]] | None = None,
|
||||
) -> str:
|
||||
"""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
|
||||
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 += [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)
|
||||
|
||||
|
||||
|
||||
@@ -408,8 +408,9 @@ async def test_a_bad_threshold_setting_falls_back_to_the_default():
|
||||
|
||||
|
||||
def test_plan_candidate_text_carries_the_steps():
|
||||
text = plan_candidate_text(description=None, body=" design ", step_texts=["Step one\n", ""])
|
||||
assert text == "design\n\nStep one"
|
||||
text = plan_candidate_text(description=None, body=" design ",
|
||||
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():
|
||||
|
||||
Reference in New Issue
Block a user