remove dead code, add tool descriptions in frontend, update changelog

This commit is contained in:
Yiorgis Gozadinos 2026-04-20 15:38:17 +03:00
parent ff8ad0879c
commit eb4e1721ce
No known key found for this signature in database
6 changed files with 9 additions and 226 deletions

View file

@ -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

View file

@ -204,6 +204,10 @@ function ToolCallIndicator({
</span>
);
}
case "cite":
return <span className="tool-query">Registering citations</span>;
case "list_documents":
return <span className="tool-query">Listing documents</span>;
default:
return <span>Processing...</span>;
}

View file

@ -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()

View file

@ -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",

View file

@ -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,
)

View file

@ -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