client.analyze routes through the rag-analysis skill; drop documents= and AnalysisResult.program. Re-record all cassettes that are relevant
This commit is contained in:
parent
d96d2eeb0f
commit
947a26b391
15 changed files with 69153 additions and 5383 deletions
|
|
@ -13,12 +13,17 @@
|
|||
|
||||
- `llm()` from the analysis sandbox. Sandbox externals are now `search` and `list_documents` only.
|
||||
- `list_documents` top-level tool from the analysis skill (still available as `await list_documents()` inside `execute_code`).
|
||||
- `documents=` kwarg on `client.analyze` (and the `--document` flag on `haiku-rag analyze` / MCP `analyze` tool). The pre-loaded `documents` Python variable inside the sandbox is no longer populated. Use `filter=` (SQL WHERE clause) to scope analysis to specific documents.
|
||||
- `AnalysisResult.program`. The per-execution programs are still tracked on `AnalysisState.executions` (the analysis skill's `execute_code` tool populates it); consumers that need the executed code should pull it from the skill state instead of the function return value.
|
||||
- `--cite` flag on `haiku-rag ask`. Citations always render after the answer now.
|
||||
- `system_prompt` kwarg on `client.ask`. No production caller used it; `config.prompts.domain_preamble` already covers the preamble use case.
|
||||
|
||||
### Changed
|
||||
|
||||
- `search.limit` default lowered from `10` to `5`. Reduces text + binary noise in vision-tool returns (picture count tracks result count after expansion + dedup); the cite path still selects from all returned chunks.
|
||||
- Search result formatter surfaces picture captions on a labelled line when a chunk's expanded refs include pictures. The OpenAI vision API has no identifier field for binary parts, so the caption is the only signal a model can use to map a description to the figure it sees.
|
||||
- `AnalysisConfig.model` defaults to `None` (was `ollama:gpt-oss/no-thinking/temp=0`). Consumers resolve via `config.analysis.model or config.qa.model`, so the analysis skill inherits the QA driving model when no `analysis` block is in YAML. Set `analysis.model` explicitly to keep the analysis pipeline on a different model from QA. **Note**: the type is now `ModelConfig | None`; external code reading `cfg.analysis.model.X` directly will need to handle the `None` case.
|
||||
- `client.ask` and `client.analyze` now route through the rag and rag-analysis skills internally (via `haiku.skills.run_skill`). Return shapes preserved: `client.ask` still returns `tuple[str, list[Citation]]`; `client.analyze` returns `AnalysisResult(answer, citations)`. The standalone QA and analysis agents under `agents/qa/` and `agents/analysis/agent.py` remain for now but are scheduled for removal once GEPA optimisation and the `--target qa` eval CLI move to the skill path.
|
||||
|
||||
### Fixed
|
||||
|
||||
|
|
|
|||
|
|
@ -20,8 +20,12 @@ class RawAnalysisResult(BaseModel):
|
|||
|
||||
|
||||
class AnalysisResult(BaseModel):
|
||||
"""Result from analysis execution with resolved citations."""
|
||||
"""Result from analysis execution with resolved citations.
|
||||
|
||||
The per-execution program(s) are tracked on ``AnalysisState.executions``
|
||||
(populated by the analysis skill's ``execute_code`` tool), not on this
|
||||
return value. Consumers that need the executed code should pull it from
|
||||
the skill state."""
|
||||
|
||||
answer: str
|
||||
program: str
|
||||
citations: list[Citation] = Field(default_factory=list)
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ from rich.progress import (
|
|||
TextColumn,
|
||||
TransferSpeedColumn,
|
||||
)
|
||||
from rich.syntax import Syntax
|
||||
|
||||
from haiku.rag.client import HaikuRAG, RebuildMode
|
||||
from haiku.rag.config import AppConfig, Config
|
||||
|
|
@ -469,14 +468,12 @@ class HaikuRAGApp: # pragma: no cover
|
|||
async def analyze(
|
||||
self,
|
||||
question: str,
|
||||
document: str | None = None,
|
||||
filter: str | None = None,
|
||||
):
|
||||
"""Answer a question using the analysis agent with code execution.
|
||||
"""Answer a question using the rag-analysis skill.
|
||||
|
||||
Args:
|
||||
question: The question to answer
|
||||
document: Optional document ID or title to pre-load
|
||||
filter: SQL WHERE clause to filter documents
|
||||
"""
|
||||
async with HaikuRAG(
|
||||
|
|
@ -485,24 +482,19 @@ class HaikuRAGApp: # pragma: no cover
|
|||
read_only=self.read_only,
|
||||
before=self.before,
|
||||
) as self.client:
|
||||
documents = [document] if document else None
|
||||
|
||||
self.console.print(f"[bold blue]Question:[/bold blue] {question}")
|
||||
self.console.print()
|
||||
self.console.print(
|
||||
"[dim]Running analysis agent with code execution...[/dim]"
|
||||
"[dim]Running analysis skill with code execution...[/dim]"
|
||||
)
|
||||
self.console.print()
|
||||
|
||||
result = await self.client.analyze(
|
||||
question, documents=documents, filter=filter
|
||||
)
|
||||
result = await self.client.analyze(question, filter=filter)
|
||||
|
||||
self.console.print("[bold yellow]Program:[/bold yellow]")
|
||||
self.console.print(Syntax(result.program, "python"))
|
||||
self.console.print()
|
||||
self.console.print("[bold green]Answer:[/bold green]")
|
||||
self.console.print(Markdown(result.answer))
|
||||
for renderable in format_citations_rich(result.citations):
|
||||
self.console.print(renderable)
|
||||
|
||||
async def research(
|
||||
self,
|
||||
|
|
|
|||
|
|
@ -385,7 +385,7 @@ def ask( # pragma: no cover
|
|||
)
|
||||
|
||||
|
||||
@_cli.command("analyze", help="Answer questions using code execution (analysis agent)")
|
||||
@_cli.command("analyze", help="Answer questions using the rag-analysis skill")
|
||||
def analyze( # pragma: no cover
|
||||
question: str = typer.Argument(
|
||||
help="The question to answer",
|
||||
|
|
@ -395,12 +395,6 @@ def analyze( # pragma: no cover
|
|||
"--db",
|
||||
help="Path to the LanceDB database file",
|
||||
),
|
||||
document: str | None = typer.Option(
|
||||
None,
|
||||
"--document",
|
||||
"-d",
|
||||
help="Document ID or title to pre-load for analysis",
|
||||
),
|
||||
filter: str | None = typer.Option(
|
||||
None,
|
||||
"--filter",
|
||||
|
|
@ -412,7 +406,6 @@ def analyze( # pragma: no cover
|
|||
asyncio.run(
|
||||
app.analyze(
|
||||
question=question,
|
||||
document=document,
|
||||
filter=filter,
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -390,12 +390,11 @@ class HaikuRAG:
|
|||
async def analyze(
|
||||
self,
|
||||
question: str,
|
||||
documents: list[str] | None = None,
|
||||
filter: str | None = None,
|
||||
) -> "AnalysisResult":
|
||||
from haiku.rag.client.agents import analyze
|
||||
|
||||
return await analyze(self, question, documents, filter)
|
||||
return await analyze(self, question, filter)
|
||||
|
||||
async def visualize_chunk(self, chunk: Chunk) -> list:
|
||||
from haiku.rag.client.search import visualize_chunk
|
||||
|
|
|
|||
|
|
@ -73,76 +73,36 @@ async def research(
|
|||
async def analyze(
|
||||
client: "HaikuRAG",
|
||||
question: str,
|
||||
documents: list[str] | None = None,
|
||||
filter: str | None = None,
|
||||
) -> "AnalysisResult":
|
||||
"""Answer a question using the analysis agent with code execution.
|
||||
"""Answer a question against the knowledge base via the rag-analysis skill.
|
||||
|
||||
The analysis agent can write and execute Python code in a sandboxed
|
||||
environment to solve problems that require computation, aggregation, or
|
||||
complex traversal across documents.
|
||||
The analysis skill exposes ``search``, ``execute_code``, and ``cite`` tools.
|
||||
The driving model decides when to reach for code (structural traversal,
|
||||
computation, aggregation) versus a direct ``search → cite → answer``.
|
||||
|
||||
Args:
|
||||
client: The HaikuRAG client.
|
||||
question: The question to answer.
|
||||
documents: Optional list of document IDs or titles to pre-load.
|
||||
filter: SQL WHERE clause to filter documents during searches.
|
||||
|
||||
Returns:
|
||||
AnalysisResult with the answer and the final consolidated program.
|
||||
AnalysisResult with the answer and resolved citations.
|
||||
"""
|
||||
from haiku.rag.agents.analysis import (
|
||||
AnalysisContext,
|
||||
AnalysisDeps,
|
||||
Sandbox,
|
||||
create_analysis_agent,
|
||||
)
|
||||
from haiku.rag.agents.analysis.models import AnalysisResult
|
||||
from haiku.rag.agents.research.models import Citation
|
||||
from haiku.rag.skills.analysis import AnalysisState, create_skill
|
||||
from haiku.rag.utils import get_model
|
||||
from haiku.skills import run_skill
|
||||
|
||||
context = AnalysisContext(filter=filter)
|
||||
|
||||
if documents:
|
||||
loaded_docs = []
|
||||
for doc_ref in documents:
|
||||
doc = await client.resolve_document(doc_ref)
|
||||
if doc:
|
||||
loaded_docs.append(doc)
|
||||
context.documents = loaded_docs if loaded_docs else None
|
||||
|
||||
sandbox = Sandbox(
|
||||
db_path=client.store.db_path,
|
||||
config=client._config,
|
||||
context=context,
|
||||
)
|
||||
deps = AnalysisDeps(
|
||||
sandbox=sandbox,
|
||||
context=context,
|
||||
)
|
||||
|
||||
agent = create_analysis_agent(client._config)
|
||||
result = await agent.run(question, deps=deps)
|
||||
|
||||
output = result.output
|
||||
seen: set[str] = set()
|
||||
citations: list[Citation] = []
|
||||
for sr in sandbox._search_results:
|
||||
if sr.chunk_id and sr.chunk_id not in seen:
|
||||
seen.add(sr.chunk_id)
|
||||
citations.append(
|
||||
Citation(
|
||||
index=len(seen),
|
||||
document_id=sr.document_id or "",
|
||||
chunk_id=sr.chunk_id,
|
||||
document_uri=sr.document_uri or "",
|
||||
document_title=sr.document_title,
|
||||
page_numbers=sr.page_numbers,
|
||||
headings=sr.headings,
|
||||
content=sr.content,
|
||||
)
|
||||
)
|
||||
return AnalysisResult(
|
||||
answer=output.answer,
|
||||
program=output.program,
|
||||
citations=citations,
|
||||
skill = create_skill(db_path=client.store.db_path, config=client._config)
|
||||
state = AnalysisState(document_filter=filter)
|
||||
model = get_model(
|
||||
client._config.analysis.model or client._config.qa.model, client._config
|
||||
)
|
||||
answer, _, _ = await run_skill(model, skill, question, state=state)
|
||||
citations = [
|
||||
state.citation_index[cid]
|
||||
for cid in state.citations
|
||||
if cid in state.citation_index
|
||||
]
|
||||
return AnalysisResult(answer=answer, citations=citations)
|
||||
|
|
|
|||
|
|
@ -226,18 +226,16 @@ def create_mcp_server(
|
|||
@mcp.tool()
|
||||
async def analyze(
|
||||
question: str,
|
||||
document: str | None = None,
|
||||
filter: str | None = None,
|
||||
) -> str:
|
||||
"""Answer complex questions using code execution (analysis agent).
|
||||
"""Answer complex questions using the rag-analysis skill.
|
||||
|
||||
Use this for questions requiring computation, aggregation, or
|
||||
complex traversal across documents. The agent can write Python
|
||||
code to search, analyze, and compute answers.
|
||||
structural traversal across documents. The skill can write and
|
||||
execute Python code in a sandboxed interpreter.
|
||||
|
||||
Args:
|
||||
question: The question to answer.
|
||||
document: Optional document ID or title to pre-load for analysis.
|
||||
filter: Optional SQL WHERE clause to filter documents.
|
||||
|
||||
Returns:
|
||||
|
|
@ -245,10 +243,9 @@ def create_mcp_server(
|
|||
"""
|
||||
try:
|
||||
async with HaikuRAG(db_path, config=config, read_only=read_only) as rag:
|
||||
documents = [document] if document else None
|
||||
result = await rag.analyze(question, documents=documents, filter=filter)
|
||||
result = await rag.analyze(question, filter=filter)
|
||||
return result.answer
|
||||
except Exception as e:
|
||||
return f"Error running analysis agent: {e!s}"
|
||||
return f"Error running analysis skill: {e!s}"
|
||||
|
||||
return mcp
|
||||
|
|
|
|||
|
|
@ -216,43 +216,3 @@ class TestClientAnalysisIntegration:
|
|||
assert len(found_labels) >= 6, (
|
||||
f"Expected at least 6 labels, found {len(found_labels)}: {found_labels}"
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.vcr()
|
||||
async def test_analyze_with_preloaded_documents(
|
||||
self, allow_model_requests, temp_db_path
|
||||
):
|
||||
"""Test analysis agent can use pre-loaded documents variable.
|
||||
|
||||
Agent program:
|
||||
if 'documents' in dir():
|
||||
for doc in documents:
|
||||
print(doc['title'], len(doc['content']))
|
||||
else:
|
||||
print('No preloaded documents')
|
||||
"""
|
||||
from haiku.rag.client import HaikuRAG
|
||||
|
||||
config = AppConfig()
|
||||
|
||||
async with HaikuRAG(temp_db_path, config=config, create=True) as client:
|
||||
await client.create_document(
|
||||
"The company was founded in 1985 by Jane Smith.",
|
||||
title="Company History",
|
||||
)
|
||||
await client.create_document(
|
||||
"Our mission is to make technology accessible to everyone.",
|
||||
title="Mission Statement",
|
||||
)
|
||||
|
||||
result = await client.analyze(
|
||||
"Using the pre-loaded documents variable, "
|
||||
"tell me when was the company founded and what is their mission?",
|
||||
documents=["Company History", "Mission Statement"],
|
||||
)
|
||||
|
||||
assert "1985" in result.answer
|
||||
assert (
|
||||
"accessible" in result.answer.lower()
|
||||
or "technology" in result.answer.lower()
|
||||
)
|
||||
|
|
|
|||
|
|
@ -27,6 +27,6 @@ class TestCodeExecution:
|
|||
|
||||
class TestAnalysisResult:
|
||||
def test_create_result(self):
|
||||
result = AnalysisResult(answer="The answer is 42", program="print(42)")
|
||||
result = AnalysisResult(answer="The answer is 42")
|
||||
assert result.answer == "The answer is 42"
|
||||
assert result.program == "print(42)"
|
||||
assert result.citations == []
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue