Skip to content

Commit 259baca

Browse files
committed
fix(monorepo): harden grounding and python startup readiness
1 parent 4a307cd commit 259baca

File tree

4 files changed

+89
-5
lines changed

4 files changed

+89
-5
lines changed

apps/api/src/ailss_api/agent.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -371,13 +371,27 @@ def _compose_answer(
371371
results: Sequence[RetrieveResult],
372372
note_excerpts: list[str | None],
373373
) -> str:
374-
segments = [f'Local baseline answer for "{user_input}":']
374+
segments: list[str] = []
375375
for result, note_excerpt in zip(results, note_excerpts, strict=True):
376+
evidence = _select_answer_evidence_text(result, note_excerpt)
377+
if evidence is None:
378+
continue
376379
label = result.title or result.path
377380
summary = result.summary or result.snippet
378-
evidence = note_excerpt or result.evidence_text or result.evidence[0].text
379381
segments.append(f"{label} ({result.path}) points to {summary}. Evidence: {evidence}")
380-
return " ".join(segments)
382+
if not segments:
383+
return ""
384+
return f'Local baseline answer for "{user_input}": ' + " ".join(segments)
385+
386+
387+
def _select_answer_evidence_text(result: RetrieveResult, note_excerpt: str | None) -> str | None:
388+
if note_excerpt:
389+
return note_excerpt
390+
if result.evidence_text:
391+
return result.evidence_text
392+
if result.evidence:
393+
return result.evidence[0].text
394+
return None
381395

382396

383397
def _read_note_excerpt(settings: Settings, note_path: str) -> str | None:

apps/api/tests/test_agent.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,45 @@ def test_agent_run_cites_hit_chunk_when_neighbors_precede_match(
195195
assert payload["citations"][0]["chunk_id"] == "docs-03-plan-1"
196196

197197

198+
def test_agent_run_fails_when_selected_results_have_no_grounding_evidence(
199+
tmp_path: Path, monkeypatch: MonkeyPatch
200+
) -> None:
201+
settings = build_settings_with_seed_data(tmp_path)
202+
monkeypatch.setattr(
203+
"ailss_api.agent.retrieve_notes",
204+
lambda request, settings: RetrieveResponse(
205+
query=request.query,
206+
mode="semantic_local",
207+
results=[
208+
RetrieveResult(
209+
path="missing.md",
210+
title="Ungrounded note",
211+
summary="Missing note file and no indexed evidence.",
212+
snippet="Indexed snippet without inspectable evidence.",
213+
evidence_text=None,
214+
evidence=[],
215+
)
216+
],
217+
usage=RetrievalUsage(latency_ms=1.0, used_chunks_k=1),
218+
),
219+
)
220+
client = TestClient(create_app(settings))
221+
222+
response = client.post(
223+
"/agent/run",
224+
json={
225+
"input": DEFAULT_AGENT_INPUT,
226+
"context": {"top_k": 1},
227+
},
228+
)
229+
230+
assert response.status_code == 200
231+
payload = response.json()
232+
assert payload["outcome"] == "failed"
233+
assert payload["answer"] is None
234+
assert payload["failure"]["code"] == "grounding_failure"
235+
236+
198237
def test_agent_run_rejects_write_request_without_apply(tmp_path: Path) -> None:
199238
settings = build_settings_with_seed_data(tmp_path)
200239
client = TestClient(create_app(settings))

packages/obsidian-plugin/src/pythonApi/pythonApiServiceController.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ type PortNegotiationResult = {
3535
shutdownSucceeded: boolean;
3636
};
3737

38+
const PYTHON_API_READY_TIMEOUT_MS = 30_000;
39+
const PYTHON_API_READY_POLL_INTERVAL_MS = 150;
40+
3841
export class PythonApiServiceController {
3942
private proc: ChildProcess | null = null;
4043
private stopRequested = false;
@@ -310,8 +313,8 @@ export class PythonApiServiceController {
310313
await waitForPythonApiHealth({
311314
host: preflight.host,
312315
port: preflight.port,
313-
timeoutMs: 5_000,
314-
pollIntervalMs: 150,
316+
timeoutMs: PYTHON_API_READY_TIMEOUT_MS,
317+
pollIntervalMs: PYTHON_API_READY_POLL_INTERVAL_MS,
315318
});
316319
} catch (error) {
317320
const message = error instanceof Error ? error.message : String(error);

packages/obsidian-plugin/test/pythonApi/pythonApiServiceController.startupHelpers.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,16 @@ vi.mock("../../src/utils/tcp.js", () => ({
44
waitForTcpPortToBeAvailable: vi.fn(),
55
}));
66

7+
vi.mock("../../src/pythonApi/client.js", () => ({
8+
requestPythonApiShutdown: vi.fn(),
9+
waitForPythonApiHealth: vi.fn(),
10+
}));
11+
712
import {
813
PythonApiServiceController,
914
type PythonApiServiceControllerDeps,
1015
} from "../../src/pythonApi/pythonApiServiceController.js";
16+
import { waitForPythonApiHealth } from "../../src/pythonApi/client.js";
1117
import { waitForTcpPortToBeAvailable } from "../../src/utils/tcp.js";
1218

1319
type TestSettings = ReturnType<PythonApiServiceControllerDeps["getSettings"]>;
@@ -32,6 +38,7 @@ type ControllerInternals = {
3238
shutdownAttempted: boolean;
3339
shutdownSucceeded: boolean;
3440
}>;
41+
waitUntilHealthy: (preflight: StartupPreflight) => Promise<void>;
3542
composePortInUseErrorMessage: (options: {
3643
host: string;
3744
port: number;
@@ -104,6 +111,7 @@ function createController(
104111

105112
describe("PythonApiServiceController startup helper branches", () => {
106113
const waitForPort = vi.mocked(waitForTcpPortToBeAvailable);
114+
const waitForHealth = vi.mocked(waitForPythonApiHealth);
107115

108116
beforeEach(() => {
109117
vi.clearAllMocks();
@@ -154,6 +162,26 @@ describe("PythonApiServiceController startup helper branches", () => {
154162
expect(saveSettings).not.toHaveBeenCalled();
155163
});
156164

165+
it("waits long enough for Python uv cold starts during readiness checks", async () => {
166+
const { controller } = createController();
167+
const internals = asInternals(controller);
168+
const preflight = await internals.prepareStartupPreflight();
169+
waitForHealth.mockResolvedValue({
170+
status: "ok",
171+
service: "ailss-api",
172+
version: "0.1.0-dev",
173+
checks: {},
174+
});
175+
176+
await expect(internals.waitUntilHealthy(preflight)).resolves.toBeUndefined();
177+
expect(waitForHealth).toHaveBeenCalledWith({
178+
host: "127.0.0.1",
179+
port: 8787,
180+
timeoutMs: 30_000,
181+
pollIntervalMs: 150,
182+
});
183+
});
184+
157185
it("returns available when the Python port is already free", async () => {
158186
const { controller } = createController();
159187
const internals = asInternals(controller);

0 commit comments

Comments
 (0)