Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -237,9 +237,11 @@ same related-node walk), and
reason-and-cite step over `gather_chat_sources`' retrieve step -- both
Event-Lineage link kinds feed the chat's context, ABAC-rechecked per
candidate post). Seeded fixture answers live in `post_chat_result` so
Ask is useful without a live LLM -- each fixture stores both "What
happened between these events?" and "Who is involved?" (Keymen, or
an explicit no-Keyman sentence). A missing orchestrator and no stored
Ask is useful without a live LLM -- each fixture stores "What
happened between these events?", "Who is involved?" (Keymen, or
an explicit no-Keyman sentence), and "What is the next commitment?"
(the seeded Calendar ticket title plus due date, or an explicit
no-commitment sentence on rec-006). A missing orchestrator and no stored
match is 503; the popup shows
`Chat unavailable (LLM orchestrator not configured)` rather than a
raw HTTP status. After that 503 the free-text Ask box is hidden and
Expand Down
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,18 @@ All notable changes to this project are documented here. Format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versioning follows
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.63.0] - 2026-08-14

### Added

- A third seeded Ask question, "What is the next commitment?", on
every fixture that already answers the first two chips. A-100
names Send Northridge Grid the revised quote due 2026-01-12;
B-200 names the Westfield specification due 2026-01-14; Riverbend
names its calendar ticket due 2026-01-09; rec-006 says it has no
open commitment. After `make seed` the popup chips connect Ask
to the same dated tickets home Calendar already lists.

## [0.62.0] - 2026-08-14

### Added
Expand Down
86 changes: 82 additions & 4 deletions backend/app/post_chat_ingestion.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
)
from lineageweave.post_chat import (
CANONICAL_CHAT_QUESTION,
CANONICAL_COMMITMENT_QUESTION,
CANONICAL_INVOLVED_QUESTION,
ChatSourceDocument,
normalize_chat_question,
Expand Down Expand Up @@ -276,11 +277,23 @@ def seeded_demo_involved_chat() -> SeededChat:
)


def seeded_demo_commitment_chat() -> SeededChat:
"""Synthetic Calendar answer for the demo public post -- not an LLM result."""
return SeededChat(
answer_text=(
"The next commitment is Send Northridge Grid the revised quote, "
"due 2026-01-12."
),
cited_titles=("Demo public post",),
)


def seeded_demo_exchanges() -> list[tuple[str, SeededChat]]:
"""Both canned Ask Q&As `make seed` writes for the demo public post."""
"""Canned Ask Q&As `make seed` writes for the demo public post."""
return [
(CANONICAL_CHAT_QUESTION, seeded_demo_chat()),
(CANONICAL_INVOLVED_QUESTION, seeded_demo_involved_chat()),
(CANONICAL_COMMITMENT_QUESTION, seeded_demo_commitment_chat()),
]


Expand All @@ -302,12 +315,22 @@ def seeded_fixture_involved_chat(post_title: str) -> SeededChat | None:
return _INVOLVED_CHATS.get(post_title)


def seeded_fixture_commitment_chat(post_title: str) -> SeededChat | None:
"""Synthetic Calendar answer for a reconstruct/calendar fixture title.

Not an LLM result. Returns None when the title has no seeded
commitment answer so an unknown question still 503s instead of
inventing a ticket.
"""
return _COMMITMENT_CHATS.get(post_title)


def seeded_fixture_exchanges(post_title: str) -> list[tuple[str, SeededChat]]:
"""Every canned Ask Q&A `make seed` writes for ``post_title``.

Order is what-happened first, then who-is-involved, so GET history
and the popup chips stay stable. Empty when the title is not a
known seed fixture.
Order is what-happened, who-is-involved, then next-commitment, so
GET history and the popup chips stay stable. Empty when the title
is not a known seed fixture.
"""
exchanges: list[tuple[str, SeededChat]] = []
happened = seeded_fixture_chat(post_title)
Expand All @@ -316,6 +339,9 @@ def seeded_fixture_exchanges(post_title: str) -> list[tuple[str, SeededChat]]:
involved = seeded_fixture_involved_chat(post_title)
if involved is not None:
exchanges.append((CANONICAL_INVOLVED_QUESTION, involved))
commitment = seeded_fixture_commitment_chat(post_title)
if commitment is not None:
exchanges.append((CANONICAL_COMMITMENT_QUESTION, commitment))
return exchanges


Expand Down Expand Up @@ -452,3 +478,55 @@ def _chat(answer: str, *cited: str) -> SeededChat:
"Follow-up on the Riverbend order confirmation",
),
}

_COMMITMENT_CHATS: dict[str, SeededChat] = {
"Initial site visit and project scope discussion": _chat(
"After the A-100 site visit the next commitment is Send Northridge "
"Grid the revised quote, due 2026-01-12.",
"Initial site visit and project scope discussion",
),
"Pricing renegotiation follow-up": _chat(
"The next commitment is Send Northridge Grid the revised quote, "
"due 2026-01-12.",
"Pricing renegotiation follow-up",
),
"Pricing renegotiation: revised quote sent": _chat(
"The revised quote is already sent; the next commitment is still "
"Send Northridge Grid the revised quote, due 2026-01-12.",
"Pricing renegotiation: revised quote sent",
),
"Delivery schedule question raised": _chat(
"The next commitment on this delivery-schedule branch is Confirm "
"the delivery window with logistics, due 2026-01-16.",
"Delivery schedule question raised",
),
"Delivery schedule confirmed with logistics": _chat(
"The delivery window is confirmed; the next open commitment on the "
"A-100 thread is Send Northridge Grid the revised quote, due 2026-01-12.",
"Delivery schedule confirmed with logistics",
),
"Unrelated: annual account review": _chat(
"This annual account review does not have an open commitment.",
"Unrelated: annual account review",
),
"Technical specification review meeting": _chat(
"The next commitment after the B-200 review meeting is Send "
"Westfield Power the revised specification, due 2026-01-14.",
"Technical specification review meeting",
),
"Specification revision requested": _chat(
"The next commitment is Send Westfield Power the revised "
"specification, due 2026-01-14.",
"Specification revision requested",
),
"Revised specification approved": _chat(
"The specification is approved; the next commitment is still Send "
"Westfield Power the revised specification, due 2026-01-14.",
"Revised specification approved",
),
"Follow-up on the Riverbend order confirmation": _chat(
"The next commitment is Send Riverbend the revised delivery "
"schedule, due 2026-01-09.",
"Follow-up on the Riverbend order confirmation",
),
}
31 changes: 31 additions & 0 deletions backend/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -558,10 +558,13 @@ def test_seed_demo_chat_surfaces_on_get_and_post_chat(client, demo_analyst_token
assert questions == [
"What happened between these events?",
"Who is involved?",
"What is the next commitment?",
]
assert "Northridge Grid" in history.json()["exchanges"][0]["answer_text"]
assert "Ada West" in history.json()["exchanges"][1]["answer_text"]
assert "Priya Nair" in history.json()["exchanges"][1]["answer_text"]
assert "Send Northridge Grid the revised quote" in history.json()["exchanges"][2]["answer_text"]
assert "2026-01-12" in history.json()["exchanges"][2]["answer_text"]

asked = client.post(
f"/api/posts/{seeded_db['public_post_id']}/chat",
Expand All @@ -580,6 +583,15 @@ def test_seed_demo_chat_surfaces_on_get_and_post_chat(client, demo_analyst_token
assert "Ada West" in involved.json()["answer_text"]
assert "Priya Nair" in involved.json()["answer_text"]

commitment = client.post(
f"/api/posts/{seeded_db['public_post_id']}/chat",
json={"question": "What's the next commitment?"},
headers={"Authorization": f"Bearer {demo_analyst_token}"},
)
assert commitment.status_code == 200, commitment.text
assert "Send Northridge Grid the revised quote" in commitment.json()["answer_text"]
assert "2026-01-12" in commitment.json()["answer_text"]


def test_seed_fixture_chats_surface_on_post_chat(client, demo_analyst_token, seeded_db) -> None:
"""The A-100 fork and calendar commitment `make seed` writes must
Expand Down Expand Up @@ -656,8 +668,18 @@ def test_seed_fixture_chats_surface_on_post_chat(client, demo_analyst_token, see
assert [row["question_text"] for row in fork_history.json()["exchanges"]] == [
"What happened between these events?",
"Who is involved?",
"What is the next commitment?",
]

fork_commitment = client.post(
f"/api/posts/{fork_id}/chat",
json={"question": "What is the next commitment?"},
headers={"Authorization": f"Bearer {demo_analyst_token}"},
)
assert fork_commitment.status_code == 200, fork_commitment.text
assert "Send Northridge Grid the revised quote" in fork_commitment.json()["answer_text"]
assert "2026-01-12" in fork_commitment.json()["answer_text"]

calendar = client.post(
f"/api/posts/{calendar_id}/chat",
json={"question": "What happened?"},
Expand All @@ -674,6 +696,15 @@ def test_seed_fixture_chats_surface_on_post_chat(client, demo_analyst_token, see
assert calendar_involved.status_code == 200, calendar_involved.text
assert "does not name a Keyman" in calendar_involved.json()["answer_text"]

calendar_commitment = client.post(
f"/api/posts/{calendar_id}/chat",
json={"question": "What is the next commitment?"},
headers={"Authorization": f"Bearer {demo_analyst_token}"},
)
assert calendar_commitment.status_code == 200, calendar_commitment.text
assert "Send Riverbend the revised delivery schedule" in calendar_commitment.json()["answer_text"]
assert "2026-01-09" in calendar_commitment.json()["answer_text"]

missing = client.post(
f"/api/posts/{seeded_db['own_private_post_id']}/chat",
json={"question": "What happened between these events?"},
Expand Down
2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "frontend",
"private": true,
"version": "0.62.0",
"version": "0.63.0",
"type": "module",
"scripts": {
"dev": "vite",
Expand Down
16 changes: 15 additions & 1 deletion frontend/src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -671,6 +671,13 @@ describe("App, authenticated", () => {
cited_post_ids: ["post-1"],
cited_posts: [{ post_id: "post-1", post_title: "Public post" }],
},
{
question_text: "What is the next commitment?",
answer_text:
"The next commitment is Send Northridge Grid the revised quote, due 2026-01-12.",
cited_post_ids: ["post-1"],
cited_posts: [{ post_id: "post-1", post_title: "Public post" }],
},
],
}),
);
Expand Down Expand Up @@ -784,8 +791,12 @@ describe("App, authenticated", () => {
expect(screen.getByText("The seeded follow-up after the site visit.")).toBeInTheDocument(),
);
expect(screen.getByText("Ada West and Priya Nair are the Keymen on this thread.")).toBeInTheDocument();
expect(
screen.getByText("The next commitment is Send Northridge Grid the revised quote, due 2026-01-12."),
).toBeInTheDocument();
expect(screen.getByRole("button", { name: /ask seeded question: what happened between these events/i })).toBeInTheDocument();
expect(screen.getByRole("button", { name: /ask seeded question: who is involved/i })).toBeInTheDocument();
expect(screen.getByRole("button", { name: /ask seeded question: what is the next commitment/i })).toBeInTheDocument();
});

it("asks a chat question and slides in the evidence panel for a cited source on click", async () => {
Expand Down Expand Up @@ -831,9 +842,12 @@ describe("App, authenticated", () => {
expect(
screen.getByText("Only seeded questions can be answered without an orchestrator."),
).toBeInTheDocument();
expect(screen.getAllByRole("button", { name: /ask seeded question/i })).toHaveLength(2);
expect(screen.getAllByRole("button", { name: /ask seeded question/i })).toHaveLength(3);
expect(screen.getByText("The seeded follow-up after the site visit.")).toBeInTheDocument();
expect(screen.getByText("Ada West and Priya Nair are the Keymen on this thread.")).toBeInTheDocument();
expect(
screen.getByText("The next commitment is Send Northridge Grid the revised quote, due 2026-01-12."),
).toBeInTheDocument();
});

it("shows a clear empty state when evaluate is 503 without an orchestrator", async () => {
Expand Down
2 changes: 1 addition & 1 deletion lineageweave/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,4 @@
"sentence_excerpts",
]

__version__ = "0.62.0"
__version__ = "0.63.0"
7 changes: 6 additions & 1 deletion lineageweave/post_chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,12 @@

CANONICAL_CHAT_QUESTION = "What happened between these events?"
CANONICAL_INVOLVED_QUESTION = "Who is involved?"
CANONICAL_COMMITMENT_QUESTION = "What is the next commitment?"

_TRAILING_PUNCT = re.compile(r"[?.!\s]+$")
_CANONICAL_QUESTION_NORM = "what happened between these events"
_INVOLVED_QUESTION_NORM = "who is involved"
_COMMITMENT_QUESTION_NORM = "what is the next commitment"


def normalize_chat_question(question: str) -> str:
Expand All @@ -40,13 +42,16 @@ def normalize_chat_question(question: str) -> str:
Seeded Ask matches this form, never a live paraphrase. ``What
happened?`` is an alias of the popup placeholder so a short type-in
still hits the stored fixture answer. ``Who's involved?`` aliases
the second seeded chip that names Keymen.
the second seeded chip that names Keymen. ``What's the next
commitment?`` aliases the third chip that names the Calendar ticket.
"""
folded = _TRAILING_PUNCT.sub("", " ".join(question.strip().lower().split()))
if folded == "what happened":
return _CANONICAL_QUESTION_NORM
if folded in {"who's involved", "who is involved here"}:
return _INVOLVED_QUESTION_NORM
if folded in {"what's the next commitment", "what is the next commitment here"}:
return _COMMITMENT_QUESTION_NORM
return folded


Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "lineageweave"
version = "0.62.0"
version = "0.63.0"
description = "Reconstructs git-branch-style lineage DAGs from scattered short records using multi-channel score fusion and LLM adjudication."
readme = "README.md"
license = { text = "MIT" }
Expand Down
4 changes: 2 additions & 2 deletions scripts/seed_demo_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -449,7 +449,7 @@ def _seed_demo_public_chat(cur, post_id) -> None:
"""Write the popup Ask answers for the demo public post.

Idempotent: re-seed replaces the same rows so GET/POST chat stay
non-empty without a live orchestrator. Writes both canned
non-empty without a live orchestrator. Writes the canned
questions so the chips are not a single prompt.
"""
from backend.app.post_chat_ingestion import seeded_demo_exchanges
Expand All @@ -464,7 +464,7 @@ def _seed_fixture_chats(cur) -> None:
Event Lineage click-through stays an empty Ask box without this
when the orchestrator is off. Idempotent -- finds existing titles
so a re-seed after the lineage insert's early-return still fills
the popup. Writes both canned questions per fixture.
the popup. Writes the canned questions per fixture.
"""
from lineageweave.fixtures import ambiguous_commitment_post, sample_records
from backend.app.post_chat_ingestion import seeded_fixture_exchanges
Expand Down
Loading
Loading