remove dead code, add tool descriptions in frontend, update changelog
This commit is contained in:
parent
ff8ad0879c
commit
eb4e1721ce
6 changed files with 9 additions and 226 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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
|
||||
Loading…
Reference in a new issue