a854656d3c
This commit addresses issues with vector database synchronization that
were causing test failures:
1. **Deletion Grace Period** (scanner.py)
- Fixed premature deletion of documents due to pagination cursor
inconsistencies in Notes API
- Implemented 2-scan verification with 1.5x scan interval grace period
(15 seconds default)
- Documents must be missing for 2 consecutive scans before deletion
- Documents that reappear are removed from deletion tracking
- Prevents false deletions during concurrent note creation/indexing
2. **Vector Sync Status Tool** (server/notes.py, models/notes.py)
- Added nc_notes_get_vector_sync_status MCP tool
- Returns indexed_count, pending_count, status, and enabled fields
- Enables tests and clients to wait for vector sync completion
- Uses lifespan context to access document queue and Qdrant client
3. **Test Improvements** (test_sampling.py, conftest.py)
- Added temporary_note_factory fixture for creating multiple test notes
- Updated all sampling tests to wait for vector sync completion
- Adjusted score_threshold to 0.0 for SimpleEmbeddingProvider
(feature hashing produces low-quality embeddings)
- Fixed CallToolResult extraction (removed ["result"] key access)
- Removed invalid @pytest.mark.asyncio markers (anyio mode)
All integration tests now pass successfully.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
175 lines
6.5 KiB
Python
175 lines
6.5 KiB
Python
"""Pydantic models for Notes app responses."""
|
|
|
|
from datetime import datetime
|
|
from typing import List, Optional
|
|
|
|
from pydantic import BaseModel, Field
|
|
|
|
from .base import BaseResponse, IdResponse, StatusResponse
|
|
|
|
|
|
class Note(BaseModel):
|
|
"""Model for a Nextcloud note."""
|
|
|
|
id: int = Field(description="Note ID")
|
|
title: str = Field(description="Note title")
|
|
content: str = Field(description="Note content in markdown")
|
|
category: str = Field(default="", description="Note category")
|
|
modified: int = Field(description="Unix timestamp of last modification")
|
|
favorite: bool = Field(
|
|
default=False, description="Whether note is marked as favorite"
|
|
)
|
|
etag: str = Field(description="ETag for versioning")
|
|
readonly: bool = Field(default=False, description="Whether note is read-only")
|
|
|
|
@property
|
|
def modified_datetime(self) -> datetime:
|
|
"""Convert Unix timestamp to datetime."""
|
|
return datetime.fromtimestamp(self.modified)
|
|
|
|
|
|
class NoteSearchResult(BaseModel):
|
|
"""Model for note search results (limited fields)."""
|
|
|
|
id: int = Field(description="Note ID")
|
|
title: str = Field(description="Note title")
|
|
category: str = Field(default="", description="Note category")
|
|
score: Optional[float] = Field(None, description="Search relevance score")
|
|
|
|
|
|
class SemanticSearchResult(BaseModel):
|
|
"""Model for semantic search results with additional metadata."""
|
|
|
|
id: int = Field(description="Note ID")
|
|
title: str = Field(description="Note title")
|
|
category: str = Field(default="", description="Note category")
|
|
excerpt: str = Field(description="Excerpt from matching chunk")
|
|
score: float = Field(description="Semantic similarity score (0-1)")
|
|
chunk_index: int = Field(description="Index of matching chunk in document")
|
|
total_chunks: int = Field(description="Total number of chunks in document")
|
|
|
|
|
|
class NotesSettings(BaseModel):
|
|
"""Model for Notes app settings."""
|
|
|
|
notesPath: str = Field(description="Path to notes directory")
|
|
fileSuffix: str = Field(description="File suffix for notes")
|
|
noteMode: str = Field(description="Note mode setting")
|
|
|
|
|
|
class CreateNoteResponse(IdResponse):
|
|
"""Response model for note creation."""
|
|
|
|
title: str = Field(description="The created note title")
|
|
category: str = Field(description="The created note category")
|
|
etag: str = Field(description="Current ETag for the created note")
|
|
|
|
|
|
class UpdateNoteResponse(BaseResponse):
|
|
"""Response model for note updates."""
|
|
|
|
id: int = Field(description="The updated note ID")
|
|
title: str = Field(description="The updated note title")
|
|
category: str = Field(description="The updated note category")
|
|
etag: str = Field(description="Current ETag for the updated note")
|
|
|
|
|
|
class DeleteNoteResponse(StatusResponse):
|
|
"""Response model for note deletion."""
|
|
|
|
deleted_id: int = Field(description="ID of the deleted note")
|
|
|
|
|
|
class AppendContentResponse(BaseResponse):
|
|
"""Response model for appending content to a note."""
|
|
|
|
id: int = Field(description="The updated note ID")
|
|
title: str = Field(description="The updated note title")
|
|
category: str = Field(description="The updated note category")
|
|
etag: str = Field(description="Current ETag for the updated note")
|
|
|
|
|
|
class SearchNotesResponse(BaseResponse):
|
|
"""Response model for note search."""
|
|
|
|
results: List[NoteSearchResult] = Field(description="Search results")
|
|
query: str = Field(description="The search query used")
|
|
total_found: int = Field(description="Total number of notes found")
|
|
|
|
|
|
class SemanticSearchNotesResponse(BaseResponse):
|
|
"""Response model for semantic search."""
|
|
|
|
results: List[SemanticSearchResult] = Field(
|
|
description="Semantic search results with similarity scores"
|
|
)
|
|
query: str = Field(description="The search query used")
|
|
total_found: int = Field(description="Total number of notes found")
|
|
search_method: str = Field(
|
|
default="semantic", description="Search method used (semantic or hybrid)"
|
|
)
|
|
|
|
|
|
class SamplingSearchResponse(BaseResponse):
|
|
"""Response from semantic search with LLM-generated answer via MCP sampling.
|
|
|
|
This response includes both a generated natural language answer (created by
|
|
the MCP client's LLM via sampling) and the source documents used to generate
|
|
that answer. Users can read the answer for quick information and review
|
|
sources for verification and deeper exploration.
|
|
|
|
Attributes:
|
|
query: The original user query
|
|
generated_answer: Natural language answer generated by client's LLM
|
|
sources: List of semantic search results used as context
|
|
total_found: Total number of matching documents found
|
|
search_method: Always "semantic_sampling" for this response type
|
|
model_used: Name of model that generated the answer (e.g., "claude-3-5-sonnet")
|
|
stop_reason: Why generation stopped ("endTurn", "maxTokens", etc.)
|
|
"""
|
|
|
|
query: str = Field(..., description="Original user query")
|
|
generated_answer: str = Field(
|
|
..., description="LLM-generated answer based on retrieved documents"
|
|
)
|
|
sources: List[SemanticSearchResult] = Field(
|
|
default_factory=list,
|
|
description="Source documents with excerpts and relevance scores",
|
|
)
|
|
total_found: int = Field(..., description="Total matching documents")
|
|
search_method: str = Field(
|
|
default="semantic_sampling", description="Search method used"
|
|
)
|
|
model_used: Optional[str] = Field(
|
|
default=None, description="Model that generated the answer"
|
|
)
|
|
stop_reason: Optional[str] = Field(
|
|
default=None, description="Reason generation stopped"
|
|
)
|
|
|
|
|
|
class VectorSyncStatusResponse(BaseResponse):
|
|
"""Response for vector sync status.
|
|
|
|
Provides information about the current state of vector sync,
|
|
including how many documents are indexed and how many are pending.
|
|
|
|
Attributes:
|
|
indexed_count: Number of documents in Qdrant vector database
|
|
pending_count: Number of documents in processing queue
|
|
status: Current sync status ("idle" or "syncing")
|
|
enabled: Whether vector sync is enabled
|
|
"""
|
|
|
|
indexed_count: int = Field(
|
|
default=0, description="Number of documents indexed in vector database"
|
|
)
|
|
pending_count: int = Field(
|
|
default=0, description="Number of documents pending processing"
|
|
)
|
|
status: str = Field(
|
|
default="disabled",
|
|
description='Sync status: "idle", "syncing", or "disabled"',
|
|
)
|
|
enabled: bool = Field(default=False, description="Whether vector sync is enabled")
|