5809ad3adb
Shared runtime - One agent entry point (runtime.invoke_agent_for_user / prepare_agent_turn) for web, email, iMessage, WhatsApp, scheduled and cron, so identity, permissions, memories, summaries and metrics match across channels. - Serialize turns per checkpoint thread; /query validates input and returns 409 while a durable run owns the conversation. Reconnectable chat (durable runs) - POST /runs creates an idempotent, owner-scoped run; GET .../events replays an ordered SSE log by cursor, so generation survives refresh and device switches. Stop cancels; disconnect only detaches. /stream-query is a compatibility adapter. - Coalesce streamed deltas (150 ms) and wake same-process subscribers instead of polling; keep buffered text when a run is stopped. - Drain runs on shutdown; serve via Hypercorn with a shutdown timeout covering the drain (and no longer in debug mode). CHAT_RUN_* settings are forwarded. Scheduled messages and cron - Atomic delivery claims, attempt history and explicit retries; ambiguous sends become "uncertain" and are never resent automatically. - Cron triggers need a run_id/Idempotency-Key; legacy jobs need an explicit owner. - Cancelling never rewrites a started delivery; series stop via tombstones. Legacy sent rows are not forked into new series. Malformed bodies return 400. History, integrations and retrieval - Message-content search, pinned conversations, JSON/Markdown export and a memory editor. - Integration status panel and YNAB data-freshness provenance. - Date-filtered document/note search with stable citations and a source viewer. Migration 20 is generated by aerich. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
165 lines
5.5 KiB
Python
165 lines
5.5 KiB
Python
from datetime import datetime, timezone
|
|
from types import SimpleNamespace
|
|
from unittest.mock import AsyncMock
|
|
from uuid import uuid4
|
|
|
|
import pytest
|
|
from langchain_core.documents import Document
|
|
from langchain_postgres import PGVector
|
|
from quart import Quart
|
|
from quart_jwt_extended import JWTManager, create_access_token
|
|
from sqlalchemy import Column
|
|
from sqlalchemy.dialects.postgresql import JSONB
|
|
|
|
from blueprints.rag.retrieval import build_filter, citation_url, document_date
|
|
from blueprints.rag import logic, source_api
|
|
from blueprints.conversation.artifacts_util import search_documents_artifact
|
|
|
|
|
|
@pytest.mark.parametrize("source", [None, "paperless", "obsidian"])
|
|
@pytest.mark.parametrize(
|
|
"start,end",
|
|
[("2026-09-01", "2026-09-30"), ("2026-09-01", None), (None, "2026-09-30")],
|
|
)
|
|
def test_filters_compile_with_actual_pgvector_operator_rules(source, start, end):
|
|
store = PGVector.__new__(PGVector)
|
|
store.EmbeddingStore = SimpleNamespace(cmetadata=Column("cmetadata", JSONB))
|
|
expression = store._create_filter_clause(build_filter(source, start, end))
|
|
assert expression is not None
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"start,end",
|
|
[
|
|
("20260901", None),
|
|
("bad", None),
|
|
("2026-10-01", "2026-09-01"),
|
|
(None, "9999-12-31"),
|
|
],
|
|
)
|
|
def test_date_filters_validate_before_vector_lookup(start, end):
|
|
with pytest.raises(ValueError):
|
|
build_filter("obsidian", start, end)
|
|
|
|
|
|
async def test_filtered_query_preserves_citations_and_excludes_unknown_dates(
|
|
monkeypatch,
|
|
):
|
|
identifier = str(uuid4())
|
|
docs = [
|
|
Document(
|
|
id=identifier,
|
|
page_content="September groceries plan",
|
|
metadata={
|
|
"source": "obsidian",
|
|
"date": "2026-09-15",
|
|
"filepath": "notes/budget.md",
|
|
},
|
|
),
|
|
Document(
|
|
id=str(uuid4()),
|
|
page_content="Undated plan",
|
|
metadata={"source": "obsidian"},
|
|
),
|
|
]
|
|
store = SimpleNamespace(amax_marginal_relevance_search=AsyncMock(return_value=docs))
|
|
monkeypatch.setattr(logic, "_get_vector_store", lambda: store)
|
|
text, results = await logic.query_vector_store(
|
|
"budget", source="obsidian", start_date="2026-09-01", end_date="2026-09-30"
|
|
)
|
|
assert results == docs[:1]
|
|
assert f"Citation: /sources/{identifier}" in text
|
|
assert store.amax_marginal_relevance_search.call_args.kwargs[
|
|
"filter"
|
|
] == build_filter("obsidian", "2026-09-01", "2026-09-30")
|
|
|
|
|
|
def test_artifact_citations_identify_exact_retrieved_chunk(monkeypatch):
|
|
monkeypatch.setenv("OBSIDIAN_VAULT_PATH", "/vault")
|
|
identifier = str(uuid4())
|
|
doc = Document(
|
|
id=identifier,
|
|
page_content="Savings target",
|
|
metadata={
|
|
"filepath": "/vault/notes/budget.md",
|
|
"h1": "Budget",
|
|
"h2": "Savings",
|
|
},
|
|
)
|
|
artifact = search_documents_artifact("target", "obsidian", [doc])
|
|
result = artifact["data"]["results"][0]
|
|
assert result["url"] == f"/sources/{identifier}"
|
|
assert result["source_id"] == identifier
|
|
assert result["path"] == "notes/budget.md"
|
|
assert result["headings"] == ["Budget", "Savings"]
|
|
|
|
|
|
def test_document_date_and_unknown_citations():
|
|
assert (
|
|
document_date(
|
|
{"created_date": datetime(2026, 9, 1, tzinfo=timezone.utc).timestamp()}
|
|
)
|
|
== "2026-09-01"
|
|
)
|
|
assert document_date({"date": "bad"}) is None
|
|
assert citation_url(Document(page_content="no stable ID")) is None
|
|
|
|
|
|
async def test_source_read_requires_auth_and_handles_missing_chunk(monkeypatch):
|
|
app = Quart(__name__)
|
|
app.config["JWT_SECRET_KEY"] = "test-secret"
|
|
JWTManager(app)
|
|
app.register_blueprint(source_api.sources_blueprint)
|
|
calls = []
|
|
|
|
def load(identifier):
|
|
calls.append(identifier)
|
|
return {
|
|
"id": identifier,
|
|
"source": "paperless",
|
|
"title": "Vet report",
|
|
"content": "Retrieved section",
|
|
"headings": [],
|
|
}
|
|
|
|
monkeypatch.setattr(source_api, "_load_chunk", load)
|
|
identifier = str(uuid4())
|
|
assert (
|
|
await app.test_client().get(f"/api/sources/{identifier}")
|
|
).status_code == 401
|
|
async with app.app_context():
|
|
token = create_access_token(identity=str(uuid4()))
|
|
headers = {"Authorization": f"Bearer {token}"}
|
|
response = await app.test_client().get(
|
|
f"/api/sources/{identifier}", headers=headers
|
|
)
|
|
assert (await response.get_json())["content"] == "Retrieved section"
|
|
assert response.headers["Cache-Control"] == "no-store"
|
|
assert (
|
|
await app.test_client().get("/api/sources/not-an-id", headers=headers)
|
|
).status_code == 404
|
|
assert calls == [identifier]
|
|
|
|
|
|
async def test_post_filter_keeps_note_matched_by_any_date_field(monkeypatch):
|
|
# The database filter matches if any date field is in range; the post-filter
|
|
# must agree or matching notes silently vanish from results.
|
|
doc = Document(
|
|
id=str(uuid4()),
|
|
page_content="Lease renewal notes",
|
|
metadata={
|
|
"source": "obsidian",
|
|
"date": "2023-05-01",
|
|
"created_at": "2024-06-12T09:00:00",
|
|
"filepath": "notes/lease.md",
|
|
},
|
|
)
|
|
store = SimpleNamespace(
|
|
amax_marginal_relevance_search=AsyncMock(return_value=[doc])
|
|
)
|
|
monkeypatch.setattr(logic, "_get_vector_store", lambda: store)
|
|
_, results = await logic.query_vector_store(
|
|
"lease", source="obsidian", start_date="2024-06-01", end_date="2024-06-30"
|
|
)
|
|
assert results == [doc]
|