From eb4e1721cedb742428abe803e8a9f3f02e362b3a Mon Sep 17 00:00:00 2001 From: Yiorgis Gozadinos Date: Mon, 20 Apr 2026 15:38:17 +0300 Subject: [PATCH] remove dead code, add tool descriptions in frontend, update changelog --- CHANGELOG.md | 6 +- app/frontend/components/Chat.tsx | 4 + .../haiku/rag/chat/widgets/context_modal.py | 95 ------------------- haiku_rag_slim/haiku/rag/tools/__init__.py | 3 - haiku_rag_slim/haiku/rag/tools/qa.py | 32 ------- tests/tools/test_qa.py | 95 ------------------- 6 files changed, 9 insertions(+), 226 deletions(-) delete mode 100644 haiku_rag_slim/haiku/rag/chat/widgets/context_modal.py delete mode 100644 haiku_rag_slim/haiku/rag/tools/qa.py delete mode 100644 tests/tools/test_qa.py diff --git a/CHANGELOG.md b/CHANGELOG.md index c5ab3b8d..756b971d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### Added - **Document virtual filesystem in analysis sandbox**: Documents mounted at `/documents/{id}/` with `metadata.json` (eager), `content.txt` (lazy), and `items.jsonl` (lazy). Standard Python `pathlib.Path` for browsing and reading document content and structure. -- **`execute_code` skill tool**: Direct code execution in the sandbox, surfaced as individual AG-UI events in the chat TUI +- **`execute_code` skill tool**: Direct code execution in the sandbox, surfaced as individual AG-UI events in the chat TUI. Items VFS uses a lazy bulk cache (~1s for 1000 documents vs 60s+ per-document queries). - **`cite` skill tool**: Explicit citation registration with per-turn tracking via `citation_index` and `citations` fields in state - **`--skill` flag for chat TUI**: `haiku-rag chat -s rag -s analysis` to enable specific skills - **`--model` overrides all agents**: Chat, QA, research, and analysis agents all use the specified model @@ -24,6 +24,8 @@ - **`list_documents` skill tool** takes no parameters — returns all documents - **Per-turn citation tracking**: `citation_index: dict[str, Citation]` (deduplicated) + `citations: list[list[str]]` (per-turn chunk IDs) replaces flat citation list - **Search rate limiting**: Skill search tool enforces `config.qa.max_searches` +- **Context expansion respects section boundaries**: Sections within the char budget are returned whole regardless of item count. Too-large sections expand bounded by section edges. Adjacent sections no longer merge — only overlapping ranges do. +- **Visualization shows full expanded section**: `visualize_chunk` expands context before resolving bounding boxes, so all pages the section spans get highlighted. ### Removed @@ -35,6 +37,8 @@ - **`create_analysis_toolset()`**: Removed unused `tools/analysis.py` module - **`qa_history`, `reports` from skill state**: Conversational context handled by the outer chat agent - **`combine_filters`, `build_document_filter`**: Removed from public API +- **`max_context_items`**: Removed from `SearchConfig` — `max_context_chars` is the sole expansion constraint +- **`QAHistoryEntry`, `tools/qa.py`**: Removed unused QA history model and relevance threshold ## [0.40.1] - 2026-04-17 diff --git a/app/frontend/components/Chat.tsx b/app/frontend/components/Chat.tsx index 9fbb3028..17a50eca 100644 --- a/app/frontend/components/Chat.tsx +++ b/app/frontend/components/Chat.tsx @@ -204,6 +204,10 @@ function ToolCallIndicator({ ); } + case "cite": + return Registering citations; + case "list_documents": + return Listing documents; default: return Processing...; } diff --git a/haiku_rag_slim/haiku/rag/chat/widgets/context_modal.py b/haiku_rag_slim/haiku/rag/chat/widgets/context_modal.py deleted file mode 100644 index 3dc74659..00000000 --- a/haiku_rag_slim/haiku/rag/chat/widgets/context_modal.py +++ /dev/null @@ -1,95 +0,0 @@ -from textual.app import ComposeResult -from textual.binding import Binding -from textual.containers import Horizontal, Vertical, VerticalScroll -from textual.screen import ModalScreen -from textual.widgets import Button, Markdown, Static - - -class ContextModal(ModalScreen): - """Modal screen for viewing session Q&A history.""" - - BINDINGS = [ - Binding("escape", "cancel", "Close", show=False), - Binding("ctrl+o", "cancel", "Close", show=False), - ] - - CSS = """ - ContextModal { - align: center middle; - background: rgba(0, 0, 0, 0.5); - } - - #context-container { - width: 70; - height: auto; - max-height: 32; - background: $surface; - border: tall $primary; - padding: 1 2; - } - - #context-header { - height: auto; - margin-bottom: 1; - } - - #context-description { - height: auto; - margin-bottom: 1; - color: $text-muted; - } - - #context-content { - height: 1fr; - max-height: 16; - scrollbar-gutter: stable; - } - - #button-row { - height: auto; - margin-top: 1; - align: right middle; - } - - #button-row Button { - margin-left: 1; - min-width: 10; - } - """ - - def __init__(self, qa_history: list | None = None) -> None: - super().__init__() - self._qa_history = qa_history or [] - - def compose(self) -> ComposeResult: - with Vertical(id="context-container"): - yield Static("[bold]Session Context[/bold]", id="context-header") - yield Static( - "Questions and answers from this session.", - id="context-description", - ) - with VerticalScroll(id="context-content"): - yield Markdown(self._get_content()) - with Horizontal(id="button-row"): - yield Button("Close", id="cancel-btn", variant="primary") - - def _get_content(self) -> str: - if not self._qa_history: - return "*No questions asked yet.*" - - parts = [] - for entry in self._qa_history: - q = getattr(entry, "question", str(entry)) - a = getattr(entry, "answer", "") - parts.append(f"**Q:** {q}\n\n**A:** {a}") - - return "\n\n---\n\n".join(parts) - - def on_button_pressed(self, event: Button.Pressed) -> None: - """Handle button presses.""" - if event.button.id == "cancel-btn": - self.action_cancel() - - def action_cancel(self) -> None: - """Cancel and close.""" - self.app.pop_screen() diff --git a/haiku_rag_slim/haiku/rag/tools/__init__.py b/haiku_rag_slim/haiku/rag/tools/__init__.py index 384ead52..156a003e 100644 --- a/haiku_rag_slim/haiku/rag/tools/__init__.py +++ b/haiku_rag_slim/haiku/rag/tools/__init__.py @@ -1,12 +1,9 @@ from haiku.rag.tools.context import RAGDeps from haiku.rag.tools.document import create_document_toolset from haiku.rag.tools.filters import build_multi_document_filter -from haiku.rag.tools.qa import PRIOR_ANSWER_RELEVANCE_THRESHOLD, QAHistoryEntry from haiku.rag.tools.search import create_search_toolset __all__ = [ - "PRIOR_ANSWER_RELEVANCE_THRESHOLD", - "QAHistoryEntry", "RAGDeps", "build_multi_document_filter", "create_document_toolset", diff --git a/haiku_rag_slim/haiku/rag/tools/qa.py b/haiku_rag_slim/haiku/rag/tools/qa.py deleted file mode 100644 index 116377ea..00000000 --- a/haiku_rag_slim/haiku/rag/tools/qa.py +++ /dev/null @@ -1,32 +0,0 @@ -from pydantic import BaseModel, Field - -from haiku.rag.agents.research.models import Citation, SearchAnswer - -PRIOR_ANSWER_RELEVANCE_THRESHOLD = 0.7 - - -class QAHistoryEntry(BaseModel): - """A Q&A pair with optional cached embedding for similarity matching.""" - - question: str - answer: str - confidence: float = 0.9 - citations: list[Citation] = Field(default_factory=list) - question_embedding: list[float] | None = Field(default=None, exclude=True) - - @property - def sources(self) -> list[str]: - """Source names for display.""" - return list( - dict.fromkeys(c.document_title or c.document_uri for c in self.citations) - ) - - def to_search_answer(self) -> SearchAnswer: - """Convert to SearchAnswer for research graph context.""" - return SearchAnswer( - query=self.question, - answer=self.answer, - confidence=self.confidence, - cited_chunks=[c.chunk_id for c in self.citations], - citations=self.citations, - ) diff --git a/tests/tools/test_qa.py b/tests/tools/test_qa.py deleted file mode 100644 index f21aee3a..00000000 --- a/tests/tools/test_qa.py +++ /dev/null @@ -1,95 +0,0 @@ -from haiku.rag.agents.research.models import Citation -from haiku.rag.tools.qa import PRIOR_ANSWER_RELEVANCE_THRESHOLD, QAHistoryEntry - - -class TestQAHistoryEntry: - """Tests for QAHistoryEntry model.""" - - def test_defaults(self): - """QAHistoryEntry has sensible defaults.""" - entry = QAHistoryEntry(question="What is X?", answer="X is Y.") - assert entry.confidence == 0.9 - assert entry.citations == [] - assert entry.question_embedding is None - - def test_sources_property(self): - """sources returns unique document titles.""" - citations = [ - Citation( - document_id="d1", - chunk_id="c1", - document_uri="doc1.md", - document_title="Document One", - content="Content 1", - ), - Citation( - document_id="d1", - chunk_id="c2", - document_uri="doc1.md", - document_title="Document One", - content="Content 2", - ), - Citation( - document_id="d2", - chunk_id="c3", - document_uri="doc2.md", - document_title="Document Two", - content="Content 3", - ), - ] - entry = QAHistoryEntry(question="Q", answer="A", citations=citations) - sources = entry.sources - assert len(sources) == 2 - assert "Document One" in sources - assert "Document Two" in sources - - def test_sources_uses_uri_as_fallback(self): - """sources uses uri when title is None.""" - citations = [ - Citation( - document_id="d1", - chunk_id="c1", - document_uri="test.md", - document_title=None, - content="Content", - ), - ] - entry = QAHistoryEntry(question="Q", answer="A", citations=citations) - assert entry.sources == ["test.md"] - - def test_to_search_answer(self): - """to_search_answer converts to SearchAnswer.""" - citation = Citation( - document_id="d1", - chunk_id="c1", - document_uri="doc1.md", - document_title="Doc", - content="Content", - ) - entry = QAHistoryEntry( - question="What is X?", - answer="X is Y.", - confidence=0.85, - citations=[citation], - ) - sa = entry.to_search_answer() - assert sa.query == "What is X?" - assert sa.answer == "X is Y." - assert sa.confidence == 0.85 - assert sa.cited_chunks == ["c1"] - assert len(sa.citations) == 1 - - def test_question_embedding_excluded_from_serialization(self): - """question_embedding is excluded from model_dump.""" - entry = QAHistoryEntry( - question="Q", - answer="A", - question_embedding=[0.1, 0.2], - ) - data = entry.model_dump() - assert "question_embedding" not in data - - -def test_prior_answer_relevance_threshold(): - """PRIOR_ANSWER_RELEVANCE_THRESHOLD is a sensible value.""" - assert 0 < PRIOR_ANSWER_RELEVANCE_THRESHOLD < 1