Move client and tool_context from toolset factories to RunContext.deps
This commit is contained in:
parent
467bcff94d
commit
05ebd781d4
25 changed files with 402 additions and 325 deletions
|
|
@ -20,7 +20,7 @@ repos:
|
|||
hooks:
|
||||
- id: ty
|
||||
name: ty check
|
||||
entry: uvx ty check
|
||||
entry: uv run ty check
|
||||
language: system
|
||||
types: [python]
|
||||
pass_filenames: false
|
||||
|
|
|
|||
|
|
@ -16,6 +16,9 @@
|
|||
|
||||
### Changed
|
||||
|
||||
- **Toolset factories decoupled from runtime dependencies**: `create_search_toolset()`, `create_qa_toolset()`, `create_document_toolset()`, `create_analysis_toolset()`, and `create_chat_agent()` no longer take `client` or `context` parameters. Instead, tool functions receive these via pydantic-ai's `RunContext.deps`. This enables toolset and agent creation at configuration time (cacheable, created once), with only lightweight deps created per-request. Deps must satisfy the `RAGDeps` protocol (`client: HaikuRAG`, `tool_context: ToolContext | None`)
|
||||
- **`ChatDeps` now includes `client`**: `ChatDeps(config=..., client=..., tool_context=...)` — the `client` field was added since it's no longer captured by the agent factory
|
||||
- **`prepare_chat_context()` helper**: Extracted from `create_chat_agent()` for idempotent namespace registration, since the agent factory no longer has access to the context
|
||||
- **Chat agent architecture**: Rebuilt on composable toolsets instead of monolithic tool definitions. Chat agent is now a thin wrapper around `create_search_toolset`, `create_document_toolset`, `create_qa_toolset`, and `create_analysis_toolset`
|
||||
- **State management simplified**: Removed `session_id`, `incoming_session_id`, and `incoming_session_context` from the state layer. `ToolContextCache` preserves all state (embeddings, summaries, QA history) on cached `ToolContext` instances, eliminating the need for module-level caches
|
||||
- **AG-UI state sync**: `ask` tool now emits `StateSnapshotEvent` instead of `StateDeltaEvent`, ensuring background summarization results are reliably delivered to clients
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ from haiku.rag.agents.chat import (
|
|||
AGUI_STATE_KEY,
|
||||
ChatDeps,
|
||||
create_chat_agent,
|
||||
prepare_chat_context,
|
||||
)
|
||||
from haiku.rag.client import HaikuRAG
|
||||
from haiku.rag.config import load_yaml_config
|
||||
|
|
@ -68,6 +69,10 @@ def get_client() -> HaikuRAG:
|
|||
return _client
|
||||
|
||||
|
||||
# Agent is created once at module level (no runtime deps needed)
|
||||
agent = create_chat_agent(Config)
|
||||
|
||||
|
||||
async def stream_chat(request: Request) -> Response:
|
||||
"""Chat streaming endpoint with AG-UI protocol.
|
||||
|
||||
|
|
@ -79,11 +84,13 @@ async def stream_chat(request: Request) -> Response:
|
|||
run_input = AGUIAdapter.build_run_input(body)
|
||||
|
||||
thread_id = getattr(run_input, "thread_id", None) or "default"
|
||||
context, _is_new = context_cache.get_or_create(thread_id)
|
||||
agent = create_chat_agent(Config, get_client(), context)
|
||||
context, is_new = context_cache.get_or_create(thread_id)
|
||||
if is_new:
|
||||
prepare_chat_context(context)
|
||||
|
||||
deps = ChatDeps(
|
||||
config=Config,
|
||||
client=get_client(),
|
||||
tool_context=context,
|
||||
state_key=AGUI_STATE_KEY,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -73,14 +73,15 @@ See [Applications](../apps.md#chat-tui) for the full TUI interface guide.
|
|||
|
||||
```python
|
||||
from haiku.rag.client import HaikuRAG
|
||||
from haiku.rag.agents.chat import create_chat_agent, ChatDeps
|
||||
from haiku.rag.agents.chat import create_chat_agent, prepare_chat_context, ChatDeps
|
||||
from haiku.rag.tools import ToolContext
|
||||
|
||||
agent = create_chat_agent(config)
|
||||
|
||||
async with HaikuRAG(path_to_db) as client:
|
||||
# Create agent with composed toolsets
|
||||
context = ToolContext()
|
||||
agent = create_chat_agent(config, client, context)
|
||||
deps = ChatDeps(config=config, tool_context=context)
|
||||
prepare_chat_context(context)
|
||||
deps = ChatDeps(config=config, client=client, tool_context=context)
|
||||
|
||||
# First question
|
||||
result = await agent.run("What is haiku.rag?", deps=deps)
|
||||
|
|
@ -105,11 +106,11 @@ from haiku.rag.agents.chat import (
|
|||
)
|
||||
|
||||
# Search-only agent
|
||||
agent = create_chat_agent(config, client, context, features=[FEATURE_SEARCH])
|
||||
agent = create_chat_agent(config, features=[FEATURE_SEARCH])
|
||||
|
||||
# All features including code analysis
|
||||
agent = create_chat_agent(
|
||||
config, client, context,
|
||||
config,
|
||||
features=[FEATURE_SEARCH, FEATURE_DOCUMENTS, FEATURE_QA, FEATURE_ANALYSIS],
|
||||
)
|
||||
```
|
||||
|
|
|
|||
|
|
@ -432,20 +432,28 @@ See [RLM Agent](agents/rlm.md) for details on capabilities and configuration.
|
|||
haiku.rag provides composable toolset factories that can be mixed into any pydantic-ai agent. This lets you build custom agents with exactly the capabilities you need — search, document management, Q&A, or code analysis — sharing state across tools via `ToolContext`.
|
||||
|
||||
```python
|
||||
from dataclasses import dataclass
|
||||
from pydantic_ai import Agent
|
||||
from haiku.rag.tools import ToolContext, create_search_toolset, create_qa_toolset
|
||||
from haiku.rag.tools import ToolContext, RAGDeps, create_search_toolset, create_qa_toolset
|
||||
|
||||
@dataclass
|
||||
class MyDeps:
|
||||
client: HaikuRAG
|
||||
tool_context: ToolContext | None = None
|
||||
|
||||
search = create_search_toolset(config)
|
||||
qa = create_qa_toolset(config)
|
||||
|
||||
agent = Agent(
|
||||
"openai:gpt-4o",
|
||||
deps_type=MyDeps,
|
||||
instructions="You are a helpful assistant.",
|
||||
toolsets=[search, qa],
|
||||
)
|
||||
|
||||
async with HaikuRAG("path/to/db.lancedb") as client:
|
||||
context = ToolContext()
|
||||
agent = Agent(
|
||||
"openai:gpt-4o",
|
||||
instructions="You are a helpful assistant.",
|
||||
toolsets=[
|
||||
create_search_toolset(client, config, context=context),
|
||||
create_qa_toolset(client, config, context=context),
|
||||
],
|
||||
)
|
||||
result = await agent.run("What are the main findings?")
|
||||
deps = MyDeps(client=client, tool_context=ToolContext())
|
||||
result = await agent.run("What are the main findings?", deps=deps)
|
||||
```
|
||||
|
||||
See [Toolsets](tools.md) for the full API reference and composition guide.
|
||||
|
|
|
|||
|
|
@ -50,19 +50,16 @@ context.load_namespace("my_namespace", MyState, data["my_namespace"])
|
|||
`create_search_toolset()` provides hybrid search (vector + full-text) with context expansion and citation tracking.
|
||||
|
||||
```python
|
||||
from haiku.rag.tools import ToolContext, create_search_toolset
|
||||
from haiku.rag.tools import create_search_toolset
|
||||
|
||||
context = ToolContext()
|
||||
search = create_search_toolset(client, config, context=context)
|
||||
search = create_search_toolset(config)
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
|
||||
| Parameter | Default | Description |
|
||||
|-----------|---------|-------------|
|
||||
| `client` | required | HaikuRAG client |
|
||||
| `config` | required | AppConfig |
|
||||
| `context` | `None` | ToolContext for state accumulation |
|
||||
| `expand_context` | `True` | Expand results with surrounding chunks |
|
||||
| `base_filter` | `None` | SQL WHERE clause applied to all searches |
|
||||
| `tool_name` | `"search"` | Name of the tool exposed to the agent |
|
||||
|
|
@ -78,19 +75,16 @@ Searches the knowledge base and returns formatted results. When a `ToolContext`
|
|||
`create_document_toolset()` provides document browsing, retrieval, and summarization.
|
||||
|
||||
```python
|
||||
from haiku.rag.tools import ToolContext, create_document_toolset
|
||||
from haiku.rag.tools import create_document_toolset
|
||||
|
||||
context = ToolContext()
|
||||
docs = create_document_toolset(client, config, context=context)
|
||||
docs = create_document_toolset(config)
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
|
||||
| Parameter | Default | Description |
|
||||
|-----------|---------|-------------|
|
||||
| `client` | required | HaikuRAG client |
|
||||
| `config` | required | AppConfig (used for summarization LLM) |
|
||||
| `context` | `None` | ToolContext for session filtering |
|
||||
| `base_filter` | `None` | SQL WHERE clause for list operations |
|
||||
|
||||
**Tools:**
|
||||
|
|
@ -104,23 +98,18 @@ docs = create_document_toolset(client, config, context=context)
|
|||
`create_qa_toolset()` provides question answering via the research graph, with prior answer recall and background summarization.
|
||||
|
||||
```python
|
||||
from haiku.rag.tools import ToolContext, create_qa_toolset
|
||||
from haiku.rag.tools import create_qa_toolset
|
||||
|
||||
context = ToolContext()
|
||||
qa = create_qa_toolset(client, config, context=context)
|
||||
qa = create_qa_toolset(config)
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
|
||||
| Parameter | Default | Description |
|
||||
|-----------|---------|-------------|
|
||||
| `client` | required | HaikuRAG client |
|
||||
| `config` | required | AppConfig |
|
||||
| `context` | `None` | ToolContext for state accumulation |
|
||||
| `base_filter` | `None` | SQL WHERE clause applied to searches |
|
||||
| `tool_name` | `"ask"` | Name of the tool exposed to the agent |
|
||||
| `session_context` | `None` | Session context for the research graph |
|
||||
| `prior_answers` | `None` | Prior answers for context |
|
||||
|
||||
**Tool: `ask(question, document_name?)`**
|
||||
|
||||
|
|
@ -162,16 +151,14 @@ for citation in result.citations:
|
|||
```python
|
||||
from haiku.rag.tools import create_analysis_toolset
|
||||
|
||||
analysis = create_analysis_toolset(client, config, context=context)
|
||||
analysis = create_analysis_toolset(config)
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
|
||||
| Parameter | Default | Description |
|
||||
|-----------|---------|-------------|
|
||||
| `client` | required | HaikuRAG client |
|
||||
| `config` | required | AppConfig |
|
||||
| `context` | `None` | ToolContext for session filtering |
|
||||
| `base_filter` | `None` | SQL WHERE clause applied to searches |
|
||||
| `tool_name` | `"analyze"` | Name of the tool exposed to the agent |
|
||||
|
||||
|
|
@ -184,41 +171,52 @@ Executes a computational task via code execution and returns an `AnalysisResult`
|
|||
Toolsets are designed to be composed into custom pydantic-ai agents:
|
||||
|
||||
```python
|
||||
from dataclasses import dataclass
|
||||
from pydantic_ai import Agent
|
||||
from haiku.rag.client import HaikuRAG
|
||||
from haiku.rag.config import Config
|
||||
from haiku.rag.tools import (
|
||||
ToolContext,
|
||||
RAGDeps,
|
||||
create_search_toolset,
|
||||
create_qa_toolset,
|
||||
create_document_toolset,
|
||||
)
|
||||
|
||||
# Toolsets are created once at configuration time
|
||||
search = create_search_toolset(Config)
|
||||
qa = create_qa_toolset(Config)
|
||||
docs = create_document_toolset(Config)
|
||||
|
||||
@dataclass
|
||||
class MyDeps:
|
||||
"""Must satisfy the RAGDeps protocol (client + tool_context)."""
|
||||
client: HaikuRAG
|
||||
tool_context: ToolContext | None = None
|
||||
|
||||
agent = Agent(
|
||||
"openai:gpt-4o",
|
||||
deps_type=MyDeps,
|
||||
instructions="You are a helpful research assistant.",
|
||||
toolsets=[search, qa, docs],
|
||||
)
|
||||
|
||||
async with HaikuRAG("path/to/db.lancedb") as client:
|
||||
# Shared context across all toolsets
|
||||
context = ToolContext()
|
||||
deps = MyDeps(client=client, tool_context=context)
|
||||
|
||||
# Pick the toolsets you need
|
||||
search = create_search_toolset(client, Config, context=context)
|
||||
qa = create_qa_toolset(client, Config, context=context)
|
||||
docs = create_document_toolset(client, Config, context=context)
|
||||
|
||||
agent = Agent(
|
||||
"openai:gpt-4o",
|
||||
instructions="You are a helpful research assistant.",
|
||||
toolsets=[search, qa, docs],
|
||||
)
|
||||
|
||||
result = await agent.run("What documents do we have about climate?")
|
||||
result = await agent.run("What documents do we have about climate?", deps=deps)
|
||||
print(result.output)
|
||||
|
||||
# Access accumulated state
|
||||
from haiku.rag.tools import SearchState, SEARCH_NAMESPACE
|
||||
from haiku.rag.tools.search import SearchState, SEARCH_NAMESPACE
|
||||
search_state = context.get(SEARCH_NAMESPACE, SearchState)
|
||||
if search_state:
|
||||
print(f"Total search results: {len(search_state.results)}")
|
||||
```
|
||||
|
||||
Tool functions access `client` and `tool_context` via pydantic-ai's `RunContext.deps`, so toolsets can be created once and reused across requests. Your deps type just needs to satisfy the `RAGDeps` protocol (have `client` and `tool_context` attributes).
|
||||
|
||||
All toolsets respect session-level document filters when a `SessionState` is registered in the context. This means setting `SessionState.document_filter` restricts all tools simultaneously.
|
||||
|
||||
## AG-UI State Management
|
||||
|
|
@ -226,16 +224,22 @@ All toolsets respect session-level document filters when a `SessionState` is reg
|
|||
When using the chat agent with [AG-UI](https://docs.ag-ui.com) streaming, `ChatDeps` implements the `StateHandler` protocol. State is emitted under a namespaced key via `state_key`:
|
||||
|
||||
```python
|
||||
from haiku.rag.agents.chat import AGUI_STATE_KEY, ChatDeps, create_chat_agent
|
||||
from haiku.rag.agents.chat import (
|
||||
AGUI_STATE_KEY, ChatDeps, create_chat_agent, prepare_chat_context,
|
||||
)
|
||||
from haiku.rag.tools import ToolContext, ToolContextCache
|
||||
|
||||
# Agent can be created once at startup
|
||||
agent = create_chat_agent(config)
|
||||
|
||||
# For multi-session apps, cache ToolContext per thread
|
||||
cache = ToolContextCache()
|
||||
context, _is_new = cache.get_or_create(thread_id)
|
||||
prepare_chat_context(context) # idempotent namespace registration
|
||||
|
||||
agent = create_chat_agent(config, client, context)
|
||||
deps = ChatDeps(
|
||||
config=config,
|
||||
client=client,
|
||||
tool_context=context,
|
||||
state_key=AGUI_STATE_KEY, # "haiku.rag.chat"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ from haiku.rag.agents.chat.agent import (
|
|||
FEATURE_SEARCH,
|
||||
ChatDeps,
|
||||
create_chat_agent,
|
||||
prepare_chat_context,
|
||||
run_chat_agent,
|
||||
trigger_background_summarization,
|
||||
)
|
||||
|
|
@ -25,6 +26,7 @@ __all__ = [
|
|||
"FEATURE_SEARCH",
|
||||
"build_chat_prompt",
|
||||
"create_chat_agent",
|
||||
"prepare_chat_context",
|
||||
"run_chat_agent",
|
||||
"trigger_background_summarization",
|
||||
"ChatDeps",
|
||||
|
|
|
|||
|
|
@ -38,10 +38,11 @@ DEFAULT_FEATURES = [FEATURE_SEARCH, FEATURE_DOCUMENTS, FEATURE_QA]
|
|||
class ChatDeps:
|
||||
"""Dependencies for chat agent.
|
||||
|
||||
Implements StateHandler protocol for AG-UI state management.
|
||||
Implements RAGDeps protocol and StateHandler protocol for AG-UI state management.
|
||||
"""
|
||||
|
||||
config: AppConfig
|
||||
client: HaikuRAG
|
||||
tool_context: ToolContext
|
||||
state_key: str | None = None
|
||||
|
||||
|
|
@ -112,20 +113,39 @@ class ChatDeps:
|
|||
qa_session_state.session_context = initial
|
||||
|
||||
|
||||
def prepare_chat_context(
|
||||
context: ToolContext,
|
||||
features: list[str] | None = None,
|
||||
) -> None:
|
||||
"""Register required namespaces in a ToolContext for chat agent use.
|
||||
|
||||
Idempotent — safe to call multiple times on the same context.
|
||||
|
||||
Args:
|
||||
context: ToolContext to prepare.
|
||||
features: List of enabled features. Defaults to DEFAULT_FEATURES.
|
||||
"""
|
||||
if features is None:
|
||||
features = DEFAULT_FEATURES
|
||||
|
||||
if context.get(SESSION_NAMESPACE, SessionState) is None:
|
||||
context.register(SESSION_NAMESPACE, SessionState())
|
||||
if context.state_key is None:
|
||||
context.state_key = AGUI_STATE_KEY
|
||||
|
||||
if FEATURE_QA in features:
|
||||
if context.get(QA_SESSION_NAMESPACE, QASessionState) is None:
|
||||
context.register(QA_SESSION_NAMESPACE, QASessionState())
|
||||
|
||||
|
||||
def create_chat_agent(
|
||||
config: AppConfig,
|
||||
client: HaikuRAG,
|
||||
context: ToolContext,
|
||||
features: list[str] | None = None,
|
||||
) -> Agent[ChatDeps, str]:
|
||||
"""Create the chat agent with composed toolsets.
|
||||
|
||||
Args:
|
||||
config: Application configuration.
|
||||
client: HaikuRAG client for database operations.
|
||||
context: ToolContext for shared state across toolsets.
|
||||
SessionState is always registered. QASessionState is
|
||||
registered only when the QA feature is active.
|
||||
features: List of features to enable. Defaults to DEFAULT_FEATURES
|
||||
(search, documents, qa). Available features: "search",
|
||||
"documents", "qa", "analysis".
|
||||
|
|
@ -136,34 +156,25 @@ def create_chat_agent(
|
|||
Example:
|
||||
async with HaikuRAG(db_path, create=True) as client:
|
||||
context = ToolContext()
|
||||
agent = create_chat_agent(config, client, context)
|
||||
deps = ChatDeps(config=config, tool_context=context)
|
||||
prepare_chat_context(context)
|
||||
agent = create_chat_agent(config)
|
||||
deps = ChatDeps(config=config, client=client, tool_context=context)
|
||||
result = await agent.run("Search for X", deps=deps)
|
||||
"""
|
||||
if features is None:
|
||||
features = DEFAULT_FEATURES
|
||||
|
||||
existing = context.get(SESSION_NAMESPACE, SessionState)
|
||||
if existing is None:
|
||||
context.register(SESSION_NAMESPACE, SessionState())
|
||||
if context.state_key is None:
|
||||
context.state_key = AGUI_STATE_KEY
|
||||
|
||||
if FEATURE_QA in features:
|
||||
if context.get(QA_SESSION_NAMESPACE, QASessionState) is None:
|
||||
context.register(QA_SESSION_NAMESPACE, QASessionState())
|
||||
|
||||
toolsets = []
|
||||
if FEATURE_SEARCH in features:
|
||||
toolsets.append(create_search_toolset(client, config, context=context))
|
||||
toolsets.append(create_search_toolset(config))
|
||||
if FEATURE_DOCUMENTS in features:
|
||||
toolsets.append(create_document_toolset(client, config, context=context))
|
||||
toolsets.append(create_document_toolset(config))
|
||||
if FEATURE_QA in features:
|
||||
toolsets.append(create_qa_toolset(client, config, context=context))
|
||||
toolsets.append(create_qa_toolset(config))
|
||||
if FEATURE_ANALYSIS in features:
|
||||
from haiku.rag.tools.analysis import create_analysis_toolset
|
||||
|
||||
toolsets.append(create_analysis_toolset(client, config, context=context))
|
||||
toolsets.append(create_analysis_toolset(config))
|
||||
|
||||
model = get_model(config.qa.model, config)
|
||||
|
||||
|
|
@ -220,6 +231,7 @@ async def run_chat_agent(
|
|||
|
||||
__all__ = [
|
||||
"create_chat_agent",
|
||||
"prepare_chat_context",
|
||||
"run_chat_agent",
|
||||
"trigger_background_summarization",
|
||||
"ChatDeps",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
from dataclasses import dataclass
|
||||
|
||||
from pydantic_ai import Agent
|
||||
from pydantic_ai.output import ToolOutput
|
||||
|
||||
from haiku.rag.agents.qa.prompts import QA_SYSTEM_PROMPT
|
||||
from haiku.rag.agents.research.models import (
|
||||
|
|
@ -15,6 +16,12 @@ from haiku.rag.tools.search import SEARCH_NAMESPACE, SearchState, create_search_
|
|||
from haiku.rag.utils import get_model
|
||||
|
||||
|
||||
@dataclass
|
||||
class _QARunDeps:
|
||||
client: HaikuRAG
|
||||
tool_context: ToolContext | None = None
|
||||
|
||||
|
||||
class QuestionAnswerAgent:
|
||||
def __init__(
|
||||
self,
|
||||
|
|
@ -25,12 +32,8 @@ class QuestionAnswerAgent:
|
|||
):
|
||||
self._client = client
|
||||
self._config = config or Config
|
||||
self._agent: Agent[None, RawSearchAnswer] = Agent(
|
||||
model=get_model(model_config, self._config),
|
||||
output_type=ToolOutput(RawSearchAnswer, max_retries=3),
|
||||
instructions=system_prompt or QA_SYSTEM_PROMPT,
|
||||
retries=3,
|
||||
)
|
||||
self._model_config = model_config
|
||||
self._system_prompt = system_prompt or QA_SYSTEM_PROMPT
|
||||
|
||||
async def answer(
|
||||
self, question: str, filter: str | None = None
|
||||
|
|
@ -44,17 +47,27 @@ class QuestionAnswerAgent:
|
|||
Returns:
|
||||
Tuple of (answer text, list of resolved citations)
|
||||
"""
|
||||
# Create context and search toolset for this run
|
||||
context = ToolContext()
|
||||
search_toolset = create_search_toolset(
|
||||
self._client,
|
||||
self._config,
|
||||
context=context,
|
||||
base_filter=filter,
|
||||
tool_name="search_documents",
|
||||
)
|
||||
|
||||
result = await self._agent.run(question, toolsets=[search_toolset])
|
||||
# Agent created per-call: toolset varies with filter, and Agent
|
||||
# construction is pure Python (no IO).
|
||||
agent = Agent(
|
||||
model=get_model(self._model_config, self._config),
|
||||
deps_type=_QARunDeps,
|
||||
output_type=RawSearchAnswer,
|
||||
output_retries=3,
|
||||
instructions=self._system_prompt,
|
||||
toolsets=[search_toolset], # ty: ignore[invalid-argument-type]
|
||||
retries=3,
|
||||
)
|
||||
|
||||
deps = _QARunDeps(client=self._client, tool_context=context)
|
||||
result = await agent.run(question, deps=deps) # ty: ignore[invalid-argument-type]
|
||||
output = result.output
|
||||
|
||||
# Get search results from context for citation resolution
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ from pydantic_ai.messages import ModelMessage
|
|||
from haiku.rag.agents.chat.agent import (
|
||||
ChatDeps,
|
||||
create_chat_agent,
|
||||
prepare_chat_context,
|
||||
trigger_background_summarization,
|
||||
)
|
||||
from haiku.rag.client import HaikuRAG
|
||||
|
|
@ -152,7 +153,8 @@ class ChatApp(App):
|
|||
|
||||
# Create tool context and agent
|
||||
self.tool_context = ToolContext()
|
||||
self.agent = create_chat_agent(self.config, self.client, self.tool_context)
|
||||
prepare_chat_context(self.tool_context)
|
||||
self.agent = create_chat_agent(self.config)
|
||||
|
||||
# Sync document filter to tool context
|
||||
session_state = self.tool_context.get(SESSION_NAMESPACE, SessionState)
|
||||
|
|
@ -250,6 +252,7 @@ class ChatApp(App):
|
|||
|
||||
deps = ChatDeps(
|
||||
config=self.config,
|
||||
client=self.client,
|
||||
tool_context=self.tool_context,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
from haiku.rag.tools.analysis import create_analysis_toolset
|
||||
from haiku.rag.tools.context import ToolContext, ToolContextCache
|
||||
from haiku.rag.tools.context import RAGDeps, ToolContext, ToolContextCache
|
||||
from haiku.rag.tools.document import (
|
||||
DocumentInfo,
|
||||
DocumentListResponse,
|
||||
|
|
@ -29,6 +29,7 @@ from haiku.rag.tools.session import (
|
|||
)
|
||||
|
||||
__all__ = [
|
||||
"RAGDeps",
|
||||
"ToolContext",
|
||||
"ToolContextCache",
|
||||
"QAResult",
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
from pydantic_ai import FunctionToolset
|
||||
from pydantic_ai import FunctionToolset, RunContext
|
||||
|
||||
from haiku.rag.agents.rlm.agent import create_rlm_agent
|
||||
from haiku.rag.agents.rlm.dependencies import RLMContext, RLMDeps
|
||||
from haiku.rag.agents.rlm.docker_sandbox import DockerSandbox
|
||||
from haiku.rag.client import HaikuRAG
|
||||
from haiku.rag.config.models import AppConfig
|
||||
from haiku.rag.tools.context import ToolContext
|
||||
from haiku.rag.tools.context import RAGDeps
|
||||
from haiku.rag.tools.filters import (
|
||||
build_document_filter,
|
||||
combine_filters,
|
||||
|
|
@ -15,20 +14,14 @@ from haiku.rag.tools.models import AnalysisResult
|
|||
|
||||
|
||||
def create_analysis_toolset(
|
||||
client: HaikuRAG,
|
||||
config: AppConfig,
|
||||
context: ToolContext | None = None,
|
||||
base_filter: str | None = None,
|
||||
tool_name: str = "analyze",
|
||||
) -> FunctionToolset:
|
||||
"""Create a toolset with code analysis capabilities via RLM agent.
|
||||
|
||||
Args:
|
||||
client: HaikuRAG client for document operations.
|
||||
config: Application configuration.
|
||||
context: Optional ToolContext for state accumulation.
|
||||
If SessionState is registered, it will be used for dynamic
|
||||
document filtering.
|
||||
base_filter: Optional base SQL WHERE clause applied to searches.
|
||||
tool_name: Name for the analyze tool. Defaults to "analyze".
|
||||
|
||||
|
|
@ -37,6 +30,7 @@ def create_analysis_toolset(
|
|||
"""
|
||||
|
||||
async def analyze(
|
||||
ctx: RunContext[RAGDeps],
|
||||
task: str,
|
||||
document_name: str | None = None,
|
||||
) -> AnalysisResult:
|
||||
|
|
@ -52,9 +46,12 @@ def create_analysis_toolset(
|
|||
Returns:
|
||||
AnalysisResult with answer and execution metadata.
|
||||
"""
|
||||
client = ctx.deps.client
|
||||
tool_context = ctx.deps.tool_context
|
||||
|
||||
doc_filter = build_document_filter(document_name) if document_name else None
|
||||
effective_filter = combine_filters(
|
||||
get_session_filter(context, base_filter), doc_filter
|
||||
get_session_filter(tool_context, base_filter), doc_filter
|
||||
)
|
||||
|
||||
rlm_context = RLMContext(filter=effective_filter)
|
||||
|
|
|
|||
|
|
@ -1,11 +1,26 @@
|
|||
from datetime import datetime, timedelta
|
||||
from typing import Any, TypeVar, overload
|
||||
from typing import TYPE_CHECKING, Any, Protocol, TypeVar, overload, runtime_checkable
|
||||
|
||||
from pydantic import BaseModel, PrivateAttr
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from haiku.rag.client import HaikuRAG
|
||||
|
||||
T = TypeVar("T", bound=BaseModel)
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class RAGDeps(Protocol):
|
||||
"""Contract for toolset dependencies injected via RunContext.
|
||||
|
||||
Any deps object passed to an agent using haiku.rag toolsets must
|
||||
provide these attributes.
|
||||
"""
|
||||
|
||||
client: "HaikuRAG"
|
||||
tool_context: "ToolContext | None"
|
||||
|
||||
|
||||
class ToolContext(BaseModel):
|
||||
"""Generic state container for haiku.rag toolsets.
|
||||
|
||||
|
|
@ -24,17 +39,17 @@ class ToolContext(BaseModel):
|
|||
SEARCH_NAMESPACE = "haiku.rag.search"
|
||||
|
||||
# In toolset factory
|
||||
def create_search_toolset(client, config, context=None):
|
||||
if context:
|
||||
state = context.get_or_create(SEARCH_NAMESPACE, SearchState)
|
||||
...
|
||||
def create_search_toolset(config):
|
||||
async def search(ctx: RunContext[RAGDeps], query: str):
|
||||
tool_context = ctx.deps.tool_context
|
||||
if tool_context:
|
||||
state = tool_context.get_or_create(SEARCH_NAMESPACE, SearchState)
|
||||
...
|
||||
|
||||
# Usage
|
||||
context = ToolContext()
|
||||
search_tools = create_search_toolset(client, config, context=context)
|
||||
|
||||
search_tools = create_search_toolset(config)
|
||||
agent = Agent(..., toolsets=[search_tools])
|
||||
await agent.run("...")
|
||||
await agent.run("...", deps=my_deps)
|
||||
|
||||
# Access accumulated state
|
||||
search_state = context.get(SEARCH_NAMESPACE)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
from pydantic import BaseModel
|
||||
from pydantic_ai import Agent, FunctionToolset
|
||||
from pydantic_ai import Agent, FunctionToolset, RunContext
|
||||
|
||||
from haiku.rag.client import HaikuRAG
|
||||
from haiku.rag.config.models import AppConfig
|
||||
from haiku.rag.tools.context import ToolContext
|
||||
from haiku.rag.tools.context import RAGDeps
|
||||
from haiku.rag.tools.filters import get_session_filter
|
||||
from haiku.rag.utils import get_model
|
||||
|
||||
|
|
@ -66,26 +66,22 @@ async def find_document(client: HaikuRAG, query: str):
|
|||
|
||||
|
||||
def create_document_toolset(
|
||||
client: HaikuRAG,
|
||||
config: AppConfig,
|
||||
context: ToolContext | None = None,
|
||||
base_filter: str | None = None,
|
||||
) -> FunctionToolset:
|
||||
"""Create a toolset with document management capabilities.
|
||||
|
||||
Args:
|
||||
client: HaikuRAG client for document operations.
|
||||
config: Application configuration (used for summarization LLM).
|
||||
context: Optional ToolContext for state tracking.
|
||||
If SessionState is registered, it will be used for dynamic
|
||||
document filtering.
|
||||
base_filter: Optional base SQL WHERE clause applied to list operations.
|
||||
|
||||
Returns:
|
||||
FunctionToolset with list_documents, get_document, summarize_document tools.
|
||||
"""
|
||||
|
||||
async def list_documents(page: int = 1) -> DocumentListResponse:
|
||||
async def list_documents(
|
||||
ctx: RunContext[RAGDeps], page: int = 1
|
||||
) -> DocumentListResponse:
|
||||
"""List available documents in the knowledge base.
|
||||
|
||||
Args:
|
||||
|
|
@ -94,10 +90,13 @@ def create_document_toolset(
|
|||
Returns:
|
||||
Paginated list of documents with metadata.
|
||||
"""
|
||||
client = ctx.deps.client
|
||||
tool_context = ctx.deps.tool_context
|
||||
|
||||
page_size = 50
|
||||
offset = (page - 1) * page_size
|
||||
|
||||
effective_filter = get_session_filter(context, base_filter)
|
||||
effective_filter = get_session_filter(tool_context, base_filter)
|
||||
|
||||
docs = await client.list_documents(
|
||||
limit=page_size, offset=offset, filter=effective_filter
|
||||
|
|
@ -119,7 +118,7 @@ def create_document_toolset(
|
|||
total_documents=total,
|
||||
)
|
||||
|
||||
async def get_document(query: str) -> str:
|
||||
async def get_document(ctx: RunContext[RAGDeps], query: str) -> str:
|
||||
"""Retrieve a specific document by title or URI.
|
||||
|
||||
Args:
|
||||
|
|
@ -128,6 +127,8 @@ def create_document_toolset(
|
|||
Returns:
|
||||
Document content and metadata, or not found message.
|
||||
"""
|
||||
client = ctx.deps.client
|
||||
|
||||
doc = await find_document(client, query)
|
||||
|
||||
if doc is None:
|
||||
|
|
@ -141,7 +142,7 @@ def create_document_toolset(
|
|||
f"**Content:**\n{doc.content}"
|
||||
)
|
||||
|
||||
async def summarize_document(query: str) -> str:
|
||||
async def summarize_document(ctx: RunContext[RAGDeps], query: str) -> str:
|
||||
"""Generate a summary of a specific document.
|
||||
|
||||
Args:
|
||||
|
|
@ -150,6 +151,8 @@ def create_document_toolset(
|
|||
Returns:
|
||||
Generated summary or not found message.
|
||||
"""
|
||||
client = ctx.deps.client
|
||||
|
||||
doc = await find_document(client, query)
|
||||
|
||||
if doc is None:
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import math
|
|||
|
||||
from ag_ui.core import EventType, StateSnapshotEvent
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic_ai import FunctionToolset, ToolReturn
|
||||
from pydantic_ai import FunctionToolset, RunContext, ToolReturn
|
||||
|
||||
from haiku.rag.agents.chat.context import trigger_background_summarization
|
||||
from haiku.rag.agents.chat.state import build_chat_state_snapshot
|
||||
|
|
@ -13,7 +13,7 @@ from haiku.rag.agents.research.state import ResearchDeps, ResearchState
|
|||
from haiku.rag.client import HaikuRAG
|
||||
from haiku.rag.config.models import AppConfig
|
||||
from haiku.rag.embeddings import get_embedder
|
||||
from haiku.rag.tools.context import ToolContext
|
||||
from haiku.rag.tools.context import RAGDeps, ToolContext
|
||||
from haiku.rag.tools.filters import (
|
||||
build_document_filter,
|
||||
combine_filters,
|
||||
|
|
@ -212,34 +212,23 @@ async def run_qa_core(
|
|||
|
||||
|
||||
def create_qa_toolset(
|
||||
client: HaikuRAG,
|
||||
config: AppConfig,
|
||||
context: ToolContext | None = None,
|
||||
base_filter: str | None = None,
|
||||
tool_name: str = "ask",
|
||||
session_context: str | None = None,
|
||||
prior_answers: list[SearchAnswer] | None = None,
|
||||
) -> FunctionToolset:
|
||||
"""Create a toolset with Q&A capabilities using research graph.
|
||||
|
||||
Args:
|
||||
client: HaikuRAG client for search operations.
|
||||
config: Application configuration.
|
||||
context: Optional ToolContext for state accumulation.
|
||||
If SessionState is registered, it will be used for dynamic
|
||||
document filtering and citation indexing.
|
||||
base_filter: Optional base SQL WHERE clause applied to searches.
|
||||
tool_name: Name for the ask tool. Defaults to "ask".
|
||||
session_context: Optional session context for the research graph.
|
||||
Overridden by QASessionState.session_context if available.
|
||||
prior_answers: Optional list of prior answers for context.
|
||||
Overridden by similarity-matched answers from QASessionState if available.
|
||||
|
||||
Returns:
|
||||
FunctionToolset with an ask tool.
|
||||
"""
|
||||
|
||||
async def ask(
|
||||
ctx: RunContext[RAGDeps],
|
||||
question: str,
|
||||
document_name: str | None = None,
|
||||
) -> ToolReturn | QAResult:
|
||||
|
|
@ -254,24 +243,25 @@ def create_qa_toolset(
|
|||
Returns:
|
||||
QAResult with answer, confidence, and citations.
|
||||
"""
|
||||
client = ctx.deps.client
|
||||
tool_context = ctx.deps.tool_context
|
||||
|
||||
session_state: SessionState | None = None
|
||||
qa_session_state: QASessionState | None = None
|
||||
state_key: str | None = None
|
||||
|
||||
if context is not None:
|
||||
session_state = context.get(SESSION_NAMESPACE, SessionState)
|
||||
qa_session_state = context.get(QA_SESSION_NAMESPACE, QASessionState)
|
||||
state_key = context.state_key
|
||||
if tool_context is not None:
|
||||
session_state = tool_context.get(SESSION_NAMESPACE, SessionState)
|
||||
qa_session_state = tool_context.get(QA_SESSION_NAMESPACE, QASessionState)
|
||||
state_key = tool_context.state_key
|
||||
|
||||
qa_result = await run_qa_core(
|
||||
client=client,
|
||||
config=config,
|
||||
question=question,
|
||||
document_name=document_name,
|
||||
context=context,
|
||||
context=tool_context,
|
||||
base_filter=base_filter,
|
||||
session_context=session_context,
|
||||
prior_answers=prior_answers,
|
||||
)
|
||||
|
||||
if session_state is not None:
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
from pydantic import BaseModel
|
||||
from pydantic_ai import FunctionToolset, ToolReturn
|
||||
from pydantic_ai import FunctionToolset, RunContext, ToolReturn
|
||||
|
||||
from haiku.rag.agents.research.models import Citation
|
||||
from haiku.rag.client import HaikuRAG
|
||||
from haiku.rag.config.models import AppConfig
|
||||
from haiku.rag.store.models import SearchResult
|
||||
from haiku.rag.tools.context import ToolContext
|
||||
from haiku.rag.tools.context import RAGDeps
|
||||
from haiku.rag.tools.filters import combine_filters, get_session_filter
|
||||
from haiku.rag.tools.session import SESSION_NAMESPACE, SessionState, compute_state_delta
|
||||
|
||||
|
|
@ -22,9 +21,7 @@ class SearchState(BaseModel):
|
|||
|
||||
|
||||
def create_search_toolset(
|
||||
client: HaikuRAG,
|
||||
config: AppConfig,
|
||||
context: ToolContext | None = None,
|
||||
expand_context: bool = True,
|
||||
base_filter: str | None = None,
|
||||
tool_name: str = "search",
|
||||
|
|
@ -32,12 +29,7 @@ def create_search_toolset(
|
|||
"""Create a toolset with search capabilities.
|
||||
|
||||
Args:
|
||||
client: HaikuRAG client for search operations.
|
||||
config: Application configuration.
|
||||
context: Optional ToolContext for state accumulation.
|
||||
If provided, search results are accumulated in SearchState.
|
||||
If SessionState is registered, it will be used for dynamic
|
||||
document filtering and citation indexing.
|
||||
expand_context: Whether to expand search results with surrounding context.
|
||||
Defaults to True.
|
||||
base_filter: Optional base SQL WHERE clause applied to all searches.
|
||||
|
|
@ -47,11 +39,9 @@ def create_search_toolset(
|
|||
Returns:
|
||||
FunctionToolset with a search tool.
|
||||
"""
|
||||
search_state: SearchState | None = None
|
||||
if context is not None:
|
||||
search_state = context.get_or_create(SEARCH_NAMESPACE, SearchState)
|
||||
|
||||
async def search(
|
||||
ctx: RunContext[RAGDeps],
|
||||
query: str,
|
||||
limit: int | None = None,
|
||||
filter: str | None = None,
|
||||
|
|
@ -66,18 +56,25 @@ def create_search_toolset(
|
|||
Returns:
|
||||
Formatted search results with content and metadata.
|
||||
"""
|
||||
client = ctx.deps.client
|
||||
tool_context = ctx.deps.tool_context
|
||||
|
||||
search_state: SearchState | None = None
|
||||
if tool_context is not None:
|
||||
search_state = tool_context.get_or_create(SEARCH_NAMESPACE, SearchState)
|
||||
|
||||
session_state: SessionState | None = None
|
||||
old_session_state: SessionState | None = None
|
||||
state_key: str | None = None
|
||||
if context is not None:
|
||||
session_state = context.get(SESSION_NAMESPACE, SessionState)
|
||||
state_key = context.state_key
|
||||
if tool_context is not None:
|
||||
session_state = tool_context.get(SESSION_NAMESPACE, SessionState)
|
||||
state_key = tool_context.state_key
|
||||
if session_state is not None:
|
||||
old_session_state = session_state.model_copy(deep=True)
|
||||
|
||||
# Combine all filters: base_filter AND session_filter AND tool filter
|
||||
effective_filter = combine_filters(
|
||||
get_session_filter(context, base_filter), filter
|
||||
get_session_filter(tool_context, base_filter), filter
|
||||
)
|
||||
|
||||
effective_limit = limit or config.search.limit
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ dev = [
|
|||
"pydantic-ai-slim[bedrock]",
|
||||
"pydantic-ai-slim[google]",
|
||||
"pydantic-ai-slim[groq]",
|
||||
"ty>=0.0.14",
|
||||
"ty>=0.0.16",
|
||||
"pytest>=9.0.2",
|
||||
"pytest-asyncio>=1.3.0",
|
||||
"pytest-cov>=7.0.0",
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ from haiku.rag.agents.chat import (
|
|||
ChatDeps,
|
||||
ChatSessionState,
|
||||
create_chat_agent,
|
||||
prepare_chat_context,
|
||||
)
|
||||
from haiku.rag.agents.chat.context import _summarization_tasks
|
||||
from haiku.rag.agents.research.models import Citation
|
||||
|
|
@ -49,22 +50,22 @@ def vcr_cassette_dir():
|
|||
|
||||
def test_create_chat_agent(temp_db_path):
|
||||
"""Test that create_chat_agent returns a properly configured agent."""
|
||||
client = HaikuRAG(temp_db_path, create=True)
|
||||
context = ToolContext()
|
||||
agent = create_chat_agent(Config, client, context)
|
||||
agent = create_chat_agent(Config)
|
||||
assert agent is not None
|
||||
assert agent.name == "chat_agent" or agent.name is None
|
||||
client.close()
|
||||
|
||||
|
||||
def test_chat_deps_initialization(temp_db_path):
|
||||
"""Test ChatDeps can be initialized with required fields."""
|
||||
client = HaikuRAG(temp_db_path, create=True)
|
||||
context = ToolContext()
|
||||
deps = ChatDeps(config=Config, tool_context=context)
|
||||
deps = ChatDeps(config=Config, client=client, tool_context=context)
|
||||
|
||||
assert deps.config is Config
|
||||
assert deps.client is client
|
||||
assert deps.tool_context is context
|
||||
assert deps.state_key is None
|
||||
client.close()
|
||||
|
||||
|
||||
def test_agui_state_key_constant():
|
||||
|
|
@ -72,33 +73,42 @@ def test_agui_state_key_constant():
|
|||
assert AGUI_STATE_KEY == "haiku.rag.chat"
|
||||
|
||||
|
||||
def test_chat_deps_with_state_key():
|
||||
def test_chat_deps_with_state_key(temp_db_path):
|
||||
"""Test ChatDeps can be initialized with state_key for keyed state emission."""
|
||||
client = HaikuRAG(temp_db_path, create=True)
|
||||
context = ToolContext()
|
||||
deps = ChatDeps(config=Config, tool_context=context, state_key="my_state")
|
||||
deps = ChatDeps(
|
||||
config=Config, client=client, tool_context=context, state_key="my_state"
|
||||
)
|
||||
|
||||
assert deps.config is Config
|
||||
assert deps.state_key == "my_state"
|
||||
client.close()
|
||||
|
||||
|
||||
def test_chat_deps_state_key_default_none():
|
||||
def test_chat_deps_state_key_default_none(temp_db_path):
|
||||
"""Test ChatDeps state_key defaults to None."""
|
||||
client = HaikuRAG(temp_db_path, create=True)
|
||||
context = ToolContext()
|
||||
deps = ChatDeps(config=Config, tool_context=context)
|
||||
deps = ChatDeps(config=Config, client=client, tool_context=context)
|
||||
|
||||
assert deps.state_key is None
|
||||
client.close()
|
||||
|
||||
|
||||
def test_chat_deps_state_setter_handles_initial_context():
|
||||
def test_chat_deps_state_setter_handles_initial_context(temp_db_path):
|
||||
"""Test ChatDeps.state setter transfers initial_context to qa_session_state."""
|
||||
from haiku.rag.tools.qa import QA_SESSION_NAMESPACE, QASessionState
|
||||
|
||||
client = HaikuRAG(temp_db_path, create=True)
|
||||
context = ToolContext()
|
||||
# Register QASessionState (normally done by create_chat_agent)
|
||||
# Register QASessionState (normally done by prepare_chat_context)
|
||||
context.register(QA_SESSION_NAMESPACE, QASessionState())
|
||||
context.register(SESSION_NAMESPACE, SessionState())
|
||||
|
||||
deps = ChatDeps(config=Config, tool_context=context, state_key=AGUI_STATE_KEY)
|
||||
deps = ChatDeps(
|
||||
config=Config, client=client, tool_context=context, state_key=AGUI_STATE_KEY
|
||||
)
|
||||
|
||||
# Client sends initial_context with no session_context
|
||||
incoming_state = {
|
||||
|
|
@ -118,17 +128,21 @@ def test_chat_deps_state_setter_handles_initial_context():
|
|||
qa_session_state = context.get(QA_SESSION_NAMESPACE)
|
||||
assert isinstance(qa_session_state, QASessionState)
|
||||
assert qa_session_state.session_context == "Background info about the project"
|
||||
client.close()
|
||||
|
||||
|
||||
def test_chat_deps_state_setter_parses_session_context_dict():
|
||||
def test_chat_deps_state_setter_parses_session_context_dict(temp_db_path):
|
||||
"""Test ChatDeps.state setter parses session_context dict and extracts summary."""
|
||||
from haiku.rag.tools.qa import QA_SESSION_NAMESPACE, QASessionState
|
||||
|
||||
client = HaikuRAG(temp_db_path, create=True)
|
||||
context = ToolContext()
|
||||
context.register(QA_SESSION_NAMESPACE, QASessionState())
|
||||
context.register(SESSION_NAMESPACE, SessionState())
|
||||
|
||||
deps = ChatDeps(config=Config, tool_context=context, state_key=AGUI_STATE_KEY)
|
||||
deps = ChatDeps(
|
||||
config=Config, client=client, tool_context=context, state_key=AGUI_STATE_KEY
|
||||
)
|
||||
|
||||
# Client sends session_context as a dict (as it comes from JSON)
|
||||
incoming_state = {
|
||||
|
|
@ -150,19 +164,23 @@ def test_chat_deps_state_setter_parses_session_context_dict():
|
|||
qa_session_state = context.get(QA_SESSION_NAMESPACE)
|
||||
assert isinstance(qa_session_state, QASessionState)
|
||||
assert qa_session_state.session_context == "Previous conversation summary"
|
||||
client.close()
|
||||
|
||||
|
||||
def test_chat_deps_state_setter_preserves_server_session_context():
|
||||
def test_chat_deps_state_setter_preserves_server_session_context(temp_db_path):
|
||||
"""Test that server's session_context is preferred over client's stale value."""
|
||||
from haiku.rag.tools.qa import QA_SESSION_NAMESPACE, QASessionState
|
||||
|
||||
client = HaikuRAG(temp_db_path, create=True)
|
||||
context = ToolContext()
|
||||
qa_state = QASessionState()
|
||||
qa_state.session_context = "Fresh summary from background summarizer"
|
||||
context.register(QA_SESSION_NAMESPACE, qa_state)
|
||||
context.register(SESSION_NAMESPACE, SessionState())
|
||||
|
||||
deps = ChatDeps(config=Config, tool_context=context, state_key=AGUI_STATE_KEY)
|
||||
deps = ChatDeps(
|
||||
config=Config, client=client, tool_context=context, state_key=AGUI_STATE_KEY
|
||||
)
|
||||
|
||||
# Client sends stale session_context
|
||||
incoming_state = {
|
||||
|
|
@ -186,6 +204,7 @@ def test_chat_deps_state_setter_preserves_server_session_context():
|
|||
assert (
|
||||
qa_session_state.session_context == "Fresh summary from background summarizer"
|
||||
)
|
||||
client.close()
|
||||
|
||||
|
||||
def test_chat_session_state():
|
||||
|
|
@ -345,9 +364,11 @@ async def test_chat_agent_search_tool(allow_model_requests, temp_db_path):
|
|||
)
|
||||
|
||||
context = ToolContext()
|
||||
agent = create_chat_agent(Config, client, context)
|
||||
prepare_chat_context(context)
|
||||
agent = create_chat_agent(Config)
|
||||
deps = ChatDeps(
|
||||
config=Config,
|
||||
client=client,
|
||||
tool_context=context,
|
||||
)
|
||||
|
||||
|
|
@ -379,9 +400,11 @@ async def test_chat_agent_search_tool_with_filter(allow_model_requests, temp_db_
|
|||
)
|
||||
|
||||
context = ToolContext()
|
||||
agent = create_chat_agent(Config, client, context)
|
||||
prepare_chat_context(context)
|
||||
agent = create_chat_agent(Config)
|
||||
deps = ChatDeps(
|
||||
config=Config,
|
||||
client=client,
|
||||
tool_context=context,
|
||||
)
|
||||
|
||||
|
|
@ -407,9 +430,11 @@ async def test_chat_agent_get_document_tool(allow_model_requests, temp_db_path):
|
|||
)
|
||||
|
||||
context = ToolContext()
|
||||
agent = create_chat_agent(Config, client, context)
|
||||
prepare_chat_context(context)
|
||||
agent = create_chat_agent(Config)
|
||||
deps = ChatDeps(
|
||||
config=Config,
|
||||
client=client,
|
||||
tool_context=context,
|
||||
)
|
||||
|
||||
|
|
@ -430,9 +455,11 @@ async def test_chat_agent_get_document_not_found(allow_model_requests, temp_db_p
|
|||
"""Test the chat agent's get_document tool when document is not found."""
|
||||
async with HaikuRAG(temp_db_path, create=True) as client:
|
||||
context = ToolContext()
|
||||
agent = create_chat_agent(Config, client, context)
|
||||
prepare_chat_context(context)
|
||||
agent = create_chat_agent(Config)
|
||||
deps = ChatDeps(
|
||||
config=Config,
|
||||
client=client,
|
||||
tool_context=context,
|
||||
)
|
||||
|
||||
|
|
@ -458,9 +485,11 @@ async def test_chat_agent_ask_adds_citations(allow_model_requests, temp_db_path)
|
|||
)
|
||||
|
||||
context = ToolContext()
|
||||
agent = create_chat_agent(Config, client, context)
|
||||
prepare_chat_context(context)
|
||||
agent = create_chat_agent(Config)
|
||||
deps = ChatDeps(
|
||||
config=Config,
|
||||
client=client,
|
||||
tool_context=context,
|
||||
state_key=AGUI_STATE_KEY,
|
||||
)
|
||||
|
|
@ -513,9 +542,11 @@ async def test_chat_agent_ask_triggers_background_summarization(
|
|||
)
|
||||
|
||||
context = ToolContext()
|
||||
agent = create_chat_agent(Config, client, context)
|
||||
prepare_chat_context(context)
|
||||
agent = create_chat_agent(Config)
|
||||
deps = ChatDeps(
|
||||
config=Config,
|
||||
client=client,
|
||||
tool_context=context,
|
||||
state_key=AGUI_STATE_KEY,
|
||||
)
|
||||
|
|
@ -577,9 +608,11 @@ async def test_chat_agent_multi_turn_with_context(allow_model_requests, temp_db_
|
|||
)
|
||||
|
||||
context = ToolContext()
|
||||
agent = create_chat_agent(Config, client, context)
|
||||
prepare_chat_context(context)
|
||||
agent = create_chat_agent(Config)
|
||||
deps = ChatDeps(
|
||||
config=Config,
|
||||
client=client,
|
||||
tool_context=context,
|
||||
state_key=AGUI_STATE_KEY,
|
||||
)
|
||||
|
|
@ -676,9 +709,11 @@ async def test_chat_agent_ask_with_prior_answer_retrieval(
|
|||
)
|
||||
|
||||
context = ToolContext()
|
||||
agent = create_chat_agent(Config, client, context)
|
||||
prepare_chat_context(context)
|
||||
agent = create_chat_agent(Config)
|
||||
deps1 = ChatDeps(
|
||||
config=Config,
|
||||
client=client,
|
||||
tool_context=context,
|
||||
state_key=AGUI_STATE_KEY,
|
||||
)
|
||||
|
|
@ -779,7 +814,8 @@ async def test_chat_agent_search_with_session_filter(
|
|||
)
|
||||
|
||||
context = ToolContext()
|
||||
agent = create_chat_agent(Config, client, context)
|
||||
prepare_chat_context(context)
|
||||
agent = create_chat_agent(Config)
|
||||
|
||||
# Set session filter to only include the labels document
|
||||
session_state = context.get(SESSION_NAMESPACE)
|
||||
|
|
@ -788,6 +824,7 @@ async def test_chat_agent_search_with_session_filter(
|
|||
|
||||
deps = ChatDeps(
|
||||
config=Config,
|
||||
client=client,
|
||||
tool_context=context,
|
||||
)
|
||||
|
||||
|
|
@ -1086,9 +1123,11 @@ async def test_list_documents_basic(allow_model_requests, temp_db_path):
|
|||
)
|
||||
|
||||
context = ToolContext()
|
||||
agent = create_chat_agent(Config, client, context)
|
||||
prepare_chat_context(context)
|
||||
agent = create_chat_agent(Config)
|
||||
deps = ChatDeps(
|
||||
config=Config,
|
||||
client=client,
|
||||
tool_context=context,
|
||||
)
|
||||
|
||||
|
|
@ -1126,9 +1165,11 @@ async def test_list_documents_with_session_filter(allow_model_requests, temp_db_
|
|||
SESSION_NAMESPACE,
|
||||
SessionState(document_filter=["DocLayNet Class Labels"]),
|
||||
)
|
||||
agent = create_chat_agent(Config, client, context)
|
||||
prepare_chat_context(context)
|
||||
agent = create_chat_agent(Config)
|
||||
deps = ChatDeps(
|
||||
config=Config,
|
||||
client=client,
|
||||
tool_context=context,
|
||||
)
|
||||
|
||||
|
|
@ -1166,9 +1207,11 @@ async def test_list_documents_pagination(allow_model_requests, temp_db_path):
|
|||
)
|
||||
|
||||
context = ToolContext()
|
||||
agent = create_chat_agent(Config, client, context)
|
||||
prepare_chat_context(context)
|
||||
agent = create_chat_agent(Config)
|
||||
deps = ChatDeps(
|
||||
config=Config,
|
||||
client=client,
|
||||
tool_context=context,
|
||||
)
|
||||
|
||||
|
|
@ -1199,9 +1242,11 @@ async def test_summarize_document_found(allow_model_requests, temp_db_path):
|
|||
)
|
||||
|
||||
context = ToolContext()
|
||||
agent = create_chat_agent(Config, client, context)
|
||||
prepare_chat_context(context)
|
||||
agent = create_chat_agent(Config)
|
||||
deps = ChatDeps(
|
||||
config=Config,
|
||||
client=client,
|
||||
tool_context=context,
|
||||
)
|
||||
|
||||
|
|
@ -1222,9 +1267,11 @@ async def test_summarize_document_not_found(allow_model_requests, temp_db_path):
|
|||
"""Test that summarize_document handles not found documents gracefully."""
|
||||
async with HaikuRAG(temp_db_path, create=True) as client:
|
||||
context = ToolContext()
|
||||
agent = create_chat_agent(Config, client, context)
|
||||
prepare_chat_context(context)
|
||||
agent = create_chat_agent(Config)
|
||||
deps = ChatDeps(
|
||||
config=Config,
|
||||
client=client,
|
||||
tool_context=context,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ from haiku.rag.agents.chat.agent import (
|
|||
FEATURE_SEARCH,
|
||||
ChatDeps,
|
||||
create_chat_agent,
|
||||
prepare_chat_context,
|
||||
)
|
||||
from haiku.rag.agents.chat.prompts import build_chat_prompt
|
||||
from haiku.rag.client import HaikuRAG
|
||||
|
|
@ -29,9 +30,9 @@ def _count_function_toolsets(agent) -> int:
|
|||
|
||||
def test_default_features(temp_db_path):
|
||||
"""Default features create search + document + qa toolsets and register both states."""
|
||||
client = HaikuRAG(temp_db_path, create=True)
|
||||
context = ToolContext()
|
||||
agent = create_chat_agent(Config, client, context)
|
||||
prepare_chat_context(context)
|
||||
agent = create_chat_agent(Config)
|
||||
|
||||
# Should have 3 toolsets (search, document, qa)
|
||||
assert _count_function_toolsets(agent) == 3
|
||||
|
|
@ -39,46 +40,42 @@ def test_default_features(temp_db_path):
|
|||
# Both SessionState and QASessionState should be registered
|
||||
assert context.get(SESSION_NAMESPACE, SessionState) is not None
|
||||
assert context.get(QA_SESSION_NAMESPACE, QASessionState) is not None
|
||||
client.close()
|
||||
|
||||
|
||||
def test_search_only(temp_db_path):
|
||||
"""features=["search"] creates only search toolset, no QASessionState."""
|
||||
client = HaikuRAG(temp_db_path, create=True)
|
||||
context = ToolContext()
|
||||
agent = create_chat_agent(Config, client, context, features=[FEATURE_SEARCH])
|
||||
prepare_chat_context(context, features=[FEATURE_SEARCH])
|
||||
agent = create_chat_agent(Config, features=[FEATURE_SEARCH])
|
||||
|
||||
assert _count_function_toolsets(agent) == 1
|
||||
|
||||
# SessionState always registered, but QASessionState should NOT be
|
||||
assert context.get(SESSION_NAMESPACE, SessionState) is not None
|
||||
assert context.get(QA_SESSION_NAMESPACE, QASessionState) is None
|
||||
client.close()
|
||||
|
||||
|
||||
def test_search_and_documents(temp_db_path):
|
||||
"""features=["search", "documents"] creates both toolsets, no QASessionState."""
|
||||
client = HaikuRAG(temp_db_path, create=True)
|
||||
context = ToolContext()
|
||||
agent = create_chat_agent(
|
||||
Config, client, context, features=[FEATURE_SEARCH, FEATURE_DOCUMENTS]
|
||||
)
|
||||
prepare_chat_context(context, features=[FEATURE_SEARCH, FEATURE_DOCUMENTS])
|
||||
agent = create_chat_agent(Config, features=[FEATURE_SEARCH, FEATURE_DOCUMENTS])
|
||||
|
||||
assert _count_function_toolsets(agent) == 2
|
||||
|
||||
assert context.get(SESSION_NAMESPACE, SessionState) is not None
|
||||
assert context.get(QA_SESSION_NAMESPACE, QASessionState) is None
|
||||
client.close()
|
||||
|
||||
|
||||
def test_all_features(temp_db_path):
|
||||
"""All four features create four toolsets."""
|
||||
client = HaikuRAG(temp_db_path, create=True)
|
||||
context = ToolContext()
|
||||
prepare_chat_context(
|
||||
context,
|
||||
features=[FEATURE_SEARCH, FEATURE_DOCUMENTS, FEATURE_QA, FEATURE_ANALYSIS],
|
||||
)
|
||||
agent = create_chat_agent(
|
||||
Config,
|
||||
client,
|
||||
context,
|
||||
features=[FEATURE_SEARCH, FEATURE_DOCUMENTS, FEATURE_QA, FEATURE_ANALYSIS],
|
||||
)
|
||||
|
||||
|
|
@ -86,28 +83,23 @@ def test_all_features(temp_db_path):
|
|||
|
||||
assert context.get(SESSION_NAMESPACE, SessionState) is not None
|
||||
assert context.get(QA_SESSION_NAMESPACE, QASessionState) is not None
|
||||
client.close()
|
||||
|
||||
|
||||
def test_no_qa_skips_qa_session_state(temp_db_path):
|
||||
"""Without QA feature, QASessionState is not registered."""
|
||||
client = HaikuRAG(temp_db_path, create=True)
|
||||
context = ToolContext()
|
||||
create_chat_agent(
|
||||
Config, client, context, features=[FEATURE_SEARCH, FEATURE_DOCUMENTS]
|
||||
)
|
||||
prepare_chat_context(context, features=[FEATURE_SEARCH, FEATURE_DOCUMENTS])
|
||||
|
||||
assert context.get(QA_SESSION_NAMESPACE, QASessionState) is None
|
||||
client.close()
|
||||
|
||||
|
||||
def test_chat_deps_state_without_qa(temp_db_path):
|
||||
"""ChatDeps.state getter omits qa_history/session_context when QASessionState absent."""
|
||||
client = HaikuRAG(temp_db_path, create=True)
|
||||
context = ToolContext()
|
||||
create_chat_agent(Config, client, context, features=[FEATURE_SEARCH])
|
||||
prepare_chat_context(context, features=[FEATURE_SEARCH])
|
||||
|
||||
deps = ChatDeps(config=Config, tool_context=context)
|
||||
deps = ChatDeps(config=Config, client=client, tool_context=context)
|
||||
state = deps.state
|
||||
|
||||
# SessionState fields should be present
|
||||
|
|
|
|||
|
|
@ -40,9 +40,7 @@ def test_get_qa_agent_with_custom_prompt(temp_db_path):
|
|||
|
||||
assert agent is not None
|
||||
assert isinstance(agent, QuestionAnswerAgent)
|
||||
# The internal pydantic-ai agent should have instructions set
|
||||
# (pydantic-ai wraps the string in an Instructions object)
|
||||
assert agent._agent.instructions is not None
|
||||
assert agent._system_prompt == custom_prompt
|
||||
|
||||
client.close()
|
||||
|
||||
|
|
|
|||
|
|
@ -6,25 +6,21 @@ from haiku.rag.tools.analysis import create_analysis_toolset
|
|||
class TestAnalysisToolset:
|
||||
"""Tests for create_analysis_toolset."""
|
||||
|
||||
def test_create_analysis_toolset_returns_function_toolset(
|
||||
self, analysis_client, analysis_config
|
||||
):
|
||||
def test_create_analysis_toolset_returns_function_toolset(self, analysis_config):
|
||||
"""create_analysis_toolset returns a FunctionToolset."""
|
||||
from pydantic_ai import FunctionToolset
|
||||
|
||||
toolset = create_analysis_toolset(analysis_client, analysis_config)
|
||||
toolset = create_analysis_toolset(analysis_config)
|
||||
assert isinstance(toolset, FunctionToolset)
|
||||
|
||||
def test_analysis_toolset_has_analyze_tool(self, analysis_client, analysis_config):
|
||||
def test_analysis_toolset_has_analyze_tool(self, analysis_config):
|
||||
"""The toolset includes an 'analyze' tool."""
|
||||
toolset = create_analysis_toolset(analysis_client, analysis_config)
|
||||
toolset = create_analysis_toolset(analysis_config)
|
||||
assert "analyze" in toolset.tools
|
||||
|
||||
def test_analysis_toolset_custom_tool_name(self, analysis_client, analysis_config):
|
||||
def test_analysis_toolset_custom_tool_name(self, analysis_config):
|
||||
"""Toolset supports custom tool name."""
|
||||
toolset = create_analysis_toolset(
|
||||
analysis_client, analysis_config, tool_name="run_code"
|
||||
)
|
||||
toolset = create_analysis_toolset(analysis_config, tool_name="run_code")
|
||||
assert "run_code" in toolset.tools
|
||||
assert "analyze" not in toolset.tools
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
from haiku.rag.tools.document import (
|
||||
|
|
@ -7,6 +9,11 @@ from haiku.rag.tools.document import (
|
|||
)
|
||||
|
||||
|
||||
def make_ctx(client, context=None):
|
||||
"""Create a lightweight RunContext-like object for direct tool function calls."""
|
||||
return SimpleNamespace(deps=SimpleNamespace(client=client, tool_context=context))
|
||||
|
||||
|
||||
class TestDocumentModels:
|
||||
"""Tests for document models."""
|
||||
|
||||
|
|
@ -38,18 +45,16 @@ class TestDocumentModels:
|
|||
class TestDocumentToolset:
|
||||
"""Tests for create_document_toolset."""
|
||||
|
||||
def test_create_document_toolset_returns_function_toolset(
|
||||
self, doc_client, doc_config
|
||||
):
|
||||
def test_create_document_toolset_returns_function_toolset(self, doc_config):
|
||||
"""create_document_toolset returns a FunctionToolset."""
|
||||
from pydantic_ai import FunctionToolset
|
||||
|
||||
toolset = create_document_toolset(doc_client, doc_config)
|
||||
toolset = create_document_toolset(doc_config)
|
||||
assert isinstance(toolset, FunctionToolset)
|
||||
|
||||
def test_document_toolset_has_expected_tools(self, doc_client, doc_config):
|
||||
def test_document_toolset_has_expected_tools(self, doc_config):
|
||||
"""The toolset includes list_documents, get_document, summarize_document."""
|
||||
toolset = create_document_toolset(doc_client, doc_config)
|
||||
toolset = create_document_toolset(doc_config)
|
||||
|
||||
assert "list_documents" in toolset.tools
|
||||
assert "get_document" in toolset.tools
|
||||
|
|
@ -65,10 +70,11 @@ class TestDocumentToolExecution:
|
|||
self, doc_client, doc_config
|
||||
):
|
||||
"""list_documents returns DocumentListResponse."""
|
||||
toolset = create_document_toolset(doc_client, doc_config)
|
||||
toolset = create_document_toolset(doc_config)
|
||||
|
||||
list_tool = toolset.tools["list_documents"]
|
||||
result = await list_tool.function()
|
||||
ctx = make_ctx(doc_client)
|
||||
result = await list_tool.function(ctx)
|
||||
|
||||
assert isinstance(result, DocumentListResponse)
|
||||
assert result.total_documents == 2
|
||||
|
|
@ -78,10 +84,11 @@ class TestDocumentToolExecution:
|
|||
@pytest.mark.asyncio
|
||||
async def test_list_documents_pagination(self, doc_client, doc_config):
|
||||
"""list_documents supports pagination."""
|
||||
toolset = create_document_toolset(doc_client, doc_config)
|
||||
toolset = create_document_toolset(doc_config)
|
||||
|
||||
list_tool = toolset.tools["list_documents"]
|
||||
result = await list_tool.function(page=2)
|
||||
ctx = make_ctx(doc_client)
|
||||
result = await list_tool.function(ctx, page=2)
|
||||
|
||||
# With only 2 documents and page_size=50, page 2 should be empty
|
||||
assert result.page == 2
|
||||
|
|
@ -90,10 +97,11 @@ class TestDocumentToolExecution:
|
|||
@pytest.mark.asyncio
|
||||
async def test_get_document_by_title(self, doc_client, doc_config):
|
||||
"""get_document finds document by title."""
|
||||
toolset = create_document_toolset(doc_client, doc_config)
|
||||
toolset = create_document_toolset(doc_config)
|
||||
|
||||
get_tool = toolset.tools["get_document"]
|
||||
result = await get_tool.function("Python Guide")
|
||||
ctx = make_ctx(doc_client)
|
||||
result = await get_tool.function(ctx, "Python Guide")
|
||||
|
||||
assert "Python Guide" in result
|
||||
assert "Python is a programming language" in result
|
||||
|
|
@ -101,20 +109,22 @@ class TestDocumentToolExecution:
|
|||
@pytest.mark.asyncio
|
||||
async def test_get_document_by_uri(self, doc_client, doc_config):
|
||||
"""get_document finds document by URI."""
|
||||
toolset = create_document_toolset(doc_client, doc_config)
|
||||
toolset = create_document_toolset(doc_config)
|
||||
|
||||
get_tool = toolset.tools["get_document"]
|
||||
result = await get_tool.function("test://python")
|
||||
ctx = make_ctx(doc_client)
|
||||
result = await get_tool.function(ctx, "test://python")
|
||||
|
||||
assert "Python Guide" in result
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_document_not_found(self, doc_client, doc_config):
|
||||
"""get_document returns appropriate message when not found."""
|
||||
toolset = create_document_toolset(doc_client, doc_config)
|
||||
toolset = create_document_toolset(doc_config)
|
||||
|
||||
get_tool = toolset.tools["get_document"]
|
||||
result = await get_tool.function("nonexistent")
|
||||
ctx = make_ctx(doc_client)
|
||||
result = await get_tool.function(ctx, "nonexistent")
|
||||
|
||||
assert "Document not found" in result
|
||||
|
||||
|
|
@ -122,11 +132,12 @@ class TestDocumentToolExecution:
|
|||
async def test_list_documents_with_base_filter(self, doc_client, doc_config):
|
||||
"""list_documents respects base_filter."""
|
||||
toolset = create_document_toolset(
|
||||
doc_client, doc_config, base_filter="title LIKE '%Python%'"
|
||||
doc_config, base_filter="title LIKE '%Python%'"
|
||||
)
|
||||
|
||||
list_tool = toolset.tools["list_documents"]
|
||||
result = await list_tool.function()
|
||||
ctx = make_ctx(doc_client)
|
||||
result = await list_tool.function(ctx)
|
||||
|
||||
assert result.total_documents == 1
|
||||
assert result.documents[0].title == "Python Guide"
|
||||
|
|
|
|||
|
|
@ -6,25 +6,21 @@ from haiku.rag.tools.qa import create_qa_toolset
|
|||
class TestQAToolset:
|
||||
"""Tests for create_qa_toolset."""
|
||||
|
||||
def test_create_qa_toolset_returns_function_toolset(
|
||||
self, qa_client_simple, qa_config
|
||||
):
|
||||
def test_create_qa_toolset_returns_function_toolset(self, qa_config):
|
||||
"""create_qa_toolset returns a FunctionToolset."""
|
||||
from pydantic_ai import FunctionToolset
|
||||
|
||||
toolset = create_qa_toolset(qa_client_simple, qa_config)
|
||||
toolset = create_qa_toolset(qa_config)
|
||||
assert isinstance(toolset, FunctionToolset)
|
||||
|
||||
def test_qa_toolset_has_ask_tool(self, qa_client_simple, qa_config):
|
||||
def test_qa_toolset_has_ask_tool(self, qa_config):
|
||||
"""The toolset includes an 'ask' tool."""
|
||||
toolset = create_qa_toolset(qa_client_simple, qa_config)
|
||||
toolset = create_qa_toolset(qa_config)
|
||||
assert "ask" in toolset.tools
|
||||
|
||||
def test_qa_toolset_custom_tool_name(self, qa_client_simple, qa_config):
|
||||
def test_qa_toolset_custom_tool_name(self, qa_config):
|
||||
"""Toolset supports custom tool name."""
|
||||
toolset = create_qa_toolset(
|
||||
qa_client_simple, qa_config, tool_name="answer_question"
|
||||
)
|
||||
toolset = create_qa_toolset(qa_config, tool_name="answer_question")
|
||||
assert "answer_question" in toolset.tools
|
||||
assert "ask" not in toolset.tools
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,16 @@
|
|||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
from haiku.rag.tools import ToolContext
|
||||
from haiku.rag.tools.search import SEARCH_NAMESPACE, SearchState, create_search_toolset
|
||||
|
||||
|
||||
def make_ctx(client, context=None):
|
||||
"""Create a lightweight RunContext-like object for direct tool function calls."""
|
||||
return SimpleNamespace(deps=SimpleNamespace(client=client, tool_context=context))
|
||||
|
||||
|
||||
class TestSearchState:
|
||||
"""Tests for SearchState model."""
|
||||
|
||||
|
|
@ -51,51 +58,20 @@ class TestSearchState:
|
|||
class TestSearchToolset:
|
||||
"""Tests for create_search_toolset."""
|
||||
|
||||
def test_create_search_toolset_returns_function_toolset(
|
||||
self, search_client, search_config
|
||||
):
|
||||
def test_create_search_toolset_returns_function_toolset(self, search_config):
|
||||
"""create_search_toolset returns a FunctionToolset."""
|
||||
from pydantic_ai import FunctionToolset
|
||||
|
||||
context = ToolContext()
|
||||
toolset = create_search_toolset(search_client, search_config, context)
|
||||
toolset = create_search_toolset(search_config)
|
||||
assert isinstance(toolset, FunctionToolset)
|
||||
|
||||
def test_search_toolset_has_search_tool(self, search_client, search_config):
|
||||
def test_search_toolset_has_search_tool(self, search_config):
|
||||
"""The toolset includes a 'search' tool."""
|
||||
context = ToolContext()
|
||||
toolset = create_search_toolset(search_client, search_config, context)
|
||||
toolset = create_search_toolset(search_config)
|
||||
|
||||
# toolset.tools is a dict with tool names as keys
|
||||
assert "search" in toolset.tools
|
||||
|
||||
def test_search_toolset_registers_state(self, search_client, search_config):
|
||||
"""Toolset registers SearchState under SEARCH_NAMESPACE."""
|
||||
context = ToolContext()
|
||||
create_search_toolset(search_client, search_config, context)
|
||||
|
||||
state = context.get(SEARCH_NAMESPACE)
|
||||
assert state is not None
|
||||
assert isinstance(state, SearchState)
|
||||
|
||||
def test_search_toolset_uses_existing_state(self, search_client, search_config):
|
||||
"""Toolset uses existing state if already registered."""
|
||||
from haiku.rag.store.models import SearchResult
|
||||
|
||||
context = ToolContext()
|
||||
existing_state = SearchState()
|
||||
existing_state.results.append(
|
||||
SearchResult(content="pre-existing", score=0.5, chunk_id="pre1")
|
||||
)
|
||||
context.register(SEARCH_NAMESPACE, existing_state)
|
||||
|
||||
create_search_toolset(search_client, search_config, context)
|
||||
|
||||
state = context.get(SEARCH_NAMESPACE)
|
||||
assert isinstance(state, SearchState)
|
||||
assert len(state.results) == 1
|
||||
assert state.results[0].chunk_id == "pre1"
|
||||
|
||||
|
||||
@pytest.mark.vcr()
|
||||
class TestSearchToolExecution:
|
||||
|
|
@ -105,11 +81,12 @@ class TestSearchToolExecution:
|
|||
async def test_search_returns_formatted_results(self, search_client, search_config):
|
||||
"""Search tool returns formatted results."""
|
||||
context = ToolContext()
|
||||
toolset = create_search_toolset(search_client, search_config, context)
|
||||
toolset = create_search_toolset(search_config)
|
||||
|
||||
# Get the search function
|
||||
search_tool = toolset.tools["search"]
|
||||
result = await search_tool.function("Python")
|
||||
ctx = make_ctx(search_client, context)
|
||||
result = await search_tool.function(ctx, "Python")
|
||||
|
||||
assert "Python" in result or "programming" in result
|
||||
assert "No results found" not in result
|
||||
|
|
@ -118,11 +95,12 @@ class TestSearchToolExecution:
|
|||
async def test_search_accumulates_in_state(self, search_client, search_config):
|
||||
"""Search tool accumulates results in SearchState."""
|
||||
context = ToolContext()
|
||||
toolset = create_search_toolset(search_client, search_config, context)
|
||||
toolset = create_search_toolset(search_config)
|
||||
|
||||
# Run search
|
||||
search_tool = toolset.tools["search"]
|
||||
await search_tool.function("Python")
|
||||
ctx = make_ctx(search_client, context)
|
||||
await search_tool.function(ctx, "Python")
|
||||
|
||||
# Check state was updated
|
||||
state = context.get(SEARCH_NAMESPACE)
|
||||
|
|
@ -138,10 +116,11 @@ class TestSearchToolExecution:
|
|||
# Use empty database
|
||||
async with HaikuRAG(temp_db_path, create=True) as empty_client:
|
||||
context = ToolContext()
|
||||
toolset = create_search_toolset(empty_client, search_config, context)
|
||||
toolset = create_search_toolset(search_config)
|
||||
|
||||
search_tool = toolset.tools["search"]
|
||||
result = await search_tool.function("anything")
|
||||
ctx = make_ctx(empty_client, context)
|
||||
result = await search_tool.function(ctx, "anything")
|
||||
|
||||
assert result == "No results found."
|
||||
|
||||
|
|
@ -149,11 +128,12 @@ class TestSearchToolExecution:
|
|||
async def test_search_with_filter(self, search_client, search_config):
|
||||
"""Search tool respects filter parameter."""
|
||||
context = ToolContext()
|
||||
toolset = create_search_toolset(search_client, search_config, context)
|
||||
toolset = create_search_toolset(search_config)
|
||||
|
||||
search_tool = toolset.tools["search"]
|
||||
ctx = make_ctx(search_client, context)
|
||||
# Filter to only Python documents
|
||||
await search_tool.function("programming", filter="title LIKE '%Python%'")
|
||||
await search_tool.function(ctx, "programming", filter="title LIKE '%Python%'")
|
||||
|
||||
# Should find Python but not JavaScript
|
||||
state = context.get(SEARCH_NAMESPACE)
|
||||
|
|
@ -164,10 +144,11 @@ class TestSearchToolExecution:
|
|||
@pytest.mark.asyncio
|
||||
async def test_search_without_context(self, search_client, search_config):
|
||||
"""Search tool works without ToolContext."""
|
||||
toolset = create_search_toolset(search_client, search_config, context=None)
|
||||
toolset = create_search_toolset(search_config)
|
||||
|
||||
search_tool = toolset.tools["search"]
|
||||
result = await search_tool.function("Python")
|
||||
ctx = make_ctx(search_client, None)
|
||||
result = await search_tool.function(ctx, "Python")
|
||||
|
||||
# Should still return results
|
||||
assert "Python" in result or "programming" in result
|
||||
|
|
@ -176,15 +157,16 @@ class TestSearchToolExecution:
|
|||
async def test_search_multiple_accumulates(self, search_client, search_config):
|
||||
"""Multiple searches accumulate results in state."""
|
||||
context = ToolContext()
|
||||
toolset = create_search_toolset(search_client, search_config, context)
|
||||
toolset = create_search_toolset(search_config)
|
||||
|
||||
search_tool = toolset.tools["search"]
|
||||
await search_tool.function("Python")
|
||||
ctx = make_ctx(search_client, context)
|
||||
await search_tool.function(ctx, "Python")
|
||||
state = context.get(SEARCH_NAMESPACE)
|
||||
assert isinstance(state, SearchState)
|
||||
first_count = len(state.results)
|
||||
|
||||
await search_tool.function("JavaScript")
|
||||
await search_tool.function(ctx, "JavaScript")
|
||||
state = context.get(SEARCH_NAMESPACE)
|
||||
assert isinstance(state, SearchState)
|
||||
second_count = len(state.results)
|
||||
|
|
@ -197,14 +179,13 @@ class TestSearchToolExecution:
|
|||
context = ToolContext()
|
||||
# Create toolset with base_filter for Python documents only
|
||||
toolset = create_search_toolset(
|
||||
search_client,
|
||||
search_config,
|
||||
context,
|
||||
base_filter="title LIKE '%Python%'",
|
||||
)
|
||||
|
||||
search_tool = toolset.tools["search"]
|
||||
await search_tool.function("programming")
|
||||
ctx = make_ctx(search_client, context)
|
||||
await search_tool.function(ctx, "programming")
|
||||
|
||||
# Should only find Python documents
|
||||
state = context.get(SEARCH_NAMESPACE)
|
||||
|
|
|
|||
38
uv.lock
38
uv.lock
|
|
@ -1335,7 +1335,7 @@ dev = [
|
|||
{ name = "pytest-cov", specifier = ">=7.0.0" },
|
||||
{ name = "pytest-recording", specifier = ">=0.13.4" },
|
||||
{ name = "ruff", specifier = ">=0.14.13" },
|
||||
{ name = "ty", specifier = ">=0.0.14" },
|
||||
{ name = "ty", specifier = ">=0.0.16" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -4986,26 +4986,26 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "ty"
|
||||
version = "0.0.14"
|
||||
version = "0.0.16"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/af/57/22c3d6bf95c2229120c49ffc2f0da8d9e8823755a1c3194da56e51f1cc31/ty-0.0.14.tar.gz", hash = "sha256:a691010565f59dd7f15cf324cdcd1d9065e010c77a04f887e1ea070ba34a7de2", size = 5036573, upload-time = "2026-01-27T00:57:31.427Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ee/18/77f84d89db54ea0d1d1b09fa2f630ac4c240c8e270761cb908c06b6e735c/ty-0.0.16.tar.gz", hash = "sha256:a999b0db6aed7d6294d036ebe43301105681e0c821a19989be7c145805d7351c", size = 5129637, upload-time = "2026-02-10T20:24:16.48Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/99/cb/cc6d1d8de59beb17a41f9a614585f884ec2d95450306c173b3b7cc090d2e/ty-0.0.14-py3-none-linux_armv6l.whl", hash = "sha256:32cf2a7596e693094621d3ae568d7ee16707dce28c34d1762947874060fdddaa", size = 10034228, upload-time = "2026-01-27T00:57:53.133Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/96/dd42816a2075a8f31542296ae687483a8d047f86a6538dfba573223eaf9a/ty-0.0.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f971bf9805f49ce8c0968ad53e29624d80b970b9eb597b7cbaba25d8a18ce9a2", size = 9939162, upload-time = "2026-01-27T00:57:43.857Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/b4/73c4859004e0f0a9eead9ecb67021438b2e8e5fdd8d03e7f5aca77623992/ty-0.0.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:45448b9e4806423523268bc15e9208c4f3f2ead7c344f615549d2e2354d6e924", size = 9418661, upload-time = "2026-01-27T00:58:03.411Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/35/839c4551b94613db4afa20ee555dd4f33bfa7352d5da74c5fa416ffa0fd2/ty-0.0.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee94a9b747ff40114085206bdb3205a631ef19a4d3fb89e302a88754cbbae54c", size = 9837872, upload-time = "2026-01-27T00:57:23.718Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/2b/bbecf7e2faa20c04bebd35fc478668953ca50ee5847ce23e08acf20ea119/ty-0.0.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6756715a3c33182e9ab8ffca2bb314d3c99b9c410b171736e145773ee0ae41c3", size = 9848819, upload-time = "2026-01-27T00:57:58.501Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/60/3c0ba0f19c0f647ad9d2b5b5ac68c0f0b4dc899001bd53b3a7537fb247a2/ty-0.0.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:89d0038a2f698ba8b6fec5cf216a4e44e2f95e4a5095a8c0f57fe549f87087c2", size = 10324371, upload-time = "2026-01-27T00:57:29.291Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/32/99d0a0b37d0397b0a989ffc2682493286aa3bc252b24004a6714368c2c3d/ty-0.0.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2c64a83a2d669b77f50a4957039ca1450626fb474619f18f6f8a3eb885bf7544", size = 10865898, upload-time = "2026-01-27T00:57:33.542Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/88/30b583a9e0311bb474269cfa91db53350557ebec09002bfc3fb3fc364e8c/ty-0.0.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:242488bfb547ef080199f6fd81369ab9cb638a778bb161511d091ffd49c12129", size = 10555777, upload-time = "2026-01-27T00:58:05.853Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/a2/cb53fb6325dcf3d40f2b1d0457a25d55bfbae633c8e337bde8ec01a190eb/ty-0.0.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4790c3866f6c83a4f424fc7d09ebdb225c1f1131647ba8bdc6fcdc28f09ed0ff", size = 10412913, upload-time = "2026-01-27T00:57:38.834Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/8f/f2f5202d725ed1e6a4e5ffaa32b190a1fe70c0b1a2503d38515da4130b4c/ty-0.0.14-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:950f320437f96d4ea9a2332bbfb5b68f1c1acd269ebfa4c09b6970cc1565bd9d", size = 9837608, upload-time = "2026-01-27T00:57:55.898Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/ba/59a2a0521640c489dafa2c546ae1f8465f92956fede18660653cce73b4c5/ty-0.0.14-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:4a0ec3ee70d83887f86925bbc1c56f4628bd58a0f47f6f32ddfe04e1f05466df", size = 9884324, upload-time = "2026-01-27T00:57:46.786Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/95/8d2a49880f47b638743212f011088552ecc454dd7a665ddcbdabea25772a/ty-0.0.14-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a1a4e6b6da0c58b34415955279eff754d6206b35af56a18bb70eb519d8d139ef", size = 10033537, upload-time = "2026-01-27T00:58:01.149Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/40/4523b36f2ce69f92ccf783855a9e0ebbbd0f0bb5cdce6211ee1737159ed3/ty-0.0.14-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:dc04384e874c5de4c5d743369c277c8aa73d1edea3c7fc646b2064b637db4db3", size = 10495910, upload-time = "2026-01-27T00:57:26.691Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/d5/655beb51224d1bfd4f9ddc0bb209659bfe71ff141bcf05c418ab670698f0/ty-0.0.14-py3-none-win32.whl", hash = "sha256:b20e22cf54c66b3e37e87377635da412d9a552c9bf4ad9fc449fed8b2e19dad2", size = 9507626, upload-time = "2026-01-27T00:57:41.43Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/d9/c569c9961760e20e0a4bc008eeb1415754564304fd53997a371b7cf3f864/ty-0.0.14-py3-none-win_amd64.whl", hash = "sha256:e312ff9475522d1a33186657fe74d1ec98e4a13e016d66f5758a452c90ff6409", size = 10437980, upload-time = "2026-01-27T00:57:36.422Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/0c/186829654f5bfd9a028f6648e9caeb11271960a61de97484627d24443f91/ty-0.0.14-py3-none-win_arm64.whl", hash = "sha256:b6facdbe9b740cb2c15293a1d178e22ffc600653646452632541d01c36d5e378", size = 9885831, upload-time = "2026-01-27T00:57:49.747Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/b9/909ebcc7f59eaf8a2c18fb54bfcf1c106f99afb3e5460058d4b46dec7b20/ty-0.0.16-py3-none-linux_armv6l.whl", hash = "sha256:6d8833b86396ed742f2b34028f51c0e98dbf010b13ae4b79d1126749dc9dab15", size = 10113870, upload-time = "2026-02-10T20:24:11.864Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/2c/b963204f3df2fdbf46a4a1ea4a060af9bb676e065d59c70ad0f5ae0dbae8/ty-0.0.16-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:934c0055d3b7f1cf3c8eab78c6c127ef7f347ff00443cef69614bda6f1502377", size = 9936286, upload-time = "2026-02-10T20:24:08.695Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/4d/3d78294f2ddfdded231e94453dea0e0adef212b2bd6536296039164c2a3e/ty-0.0.16-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b55e8e8733b416d914003cd22e831e139f034681b05afed7e951cc1a5ea1b8d4", size = 9442660, upload-time = "2026-02-10T20:24:02.704Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/40/ce48c0541e3b5749b0890725870769904e6b043e077d4710e5325d5cf807/ty-0.0.16-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:feccae8f4abd6657de111353bd604f36e164844466346eb81ffee2c2b06ea0f0", size = 9934506, upload-time = "2026-02-10T20:24:35.818Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/16/3b29de57e1ec6e56f50a4bb625ee0923edb058c5f53e29014873573a00cd/ty-0.0.16-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1cad5e29d8765b92db5fa284940ac57149561f3f89470b363b9aab8a6ce553b0", size = 9933099, upload-time = "2026-02-10T20:24:43.003Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/a1/e546995c25563d318c502b2f42af0fdbed91e1fc343708241e2076373644/ty-0.0.16-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:86f28797c7dc06f081238270b533bf4fc8e93852f34df49fb660e0b58a5cda9a", size = 10438370, upload-time = "2026-02-10T20:24:33.44Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/c1/22d301a4b2cce0f75ae84d07a495f87da193bcb68e096d43695a815c4708/ty-0.0.16-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be971a3b42bcae44d0e5787f88156ed2102ad07558c05a5ae4bfd32a99118e66", size = 10992160, upload-time = "2026-02-10T20:24:25.574Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/40/f1892b8c890db3f39a1bab8ec459b572de2df49e76d3cad2a9a239adcde9/ty-0.0.16-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3c9f982b7c4250eb91af66933f436b3a2363c24b6353e94992eab6551166c8b7", size = 10717892, upload-time = "2026-02-10T20:24:05.914Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/1b/caf9be8d0c738983845f503f2e92ea64b8d5fae1dd5ca98c3fca4aa7dadc/ty-0.0.16-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d122edf85ce7bdf6f85d19158c991d858fc835677bd31ca46319c4913043dc84", size = 10510916, upload-time = "2026-02-10T20:24:00.252Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/ea/28980f5c7e1f4c9c44995811ea6a36f2fcb205232a6ae0f5b60b11504621/ty-0.0.16-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:497ebdddbb0e35c7758ded5aa4c6245e8696a69d531d5c9b0c1a28a075374241", size = 9908506, upload-time = "2026-02-10T20:24:28.133Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/80/8672306596349463c21644554f935ff8720679a14fd658fef658f66da944/ty-0.0.16-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e1e0ac0837bde634b030243aeba8499383c0487e08f22e80f5abdacb5b0bd8ce", size = 9949486, upload-time = "2026-02-10T20:24:18.62Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/8a/d8747d36f30bd82ea157835f5b70d084c9bb5d52dd9491dba8a149792d6a/ty-0.0.16-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1216c9bcca551d9f89f47a817ebc80e88ac37683d71504e5509a6445f24fd024", size = 10145269, upload-time = "2026-02-10T20:24:38.249Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/4c/753535acc7243570c259158b7df67e9c9dd7dab9a21ee110baa4cdcec45d/ty-0.0.16-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:221bbdd2c6ee558452c96916ab67fcc465b86967cf0482e19571d18f9c831828", size = 10608644, upload-time = "2026-02-10T20:24:40.565Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/05/8e8db64cf45a8b16757e907f7a3bfde8d6203e4769b11b64e28d5bdcd79a/ty-0.0.16-py3-none-win32.whl", hash = "sha256:d52c4eb786be878e7514cab637200af607216fcc5539a06d26573ea496b26512", size = 9582579, upload-time = "2026-02-10T20:24:30.406Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/bc/45759faea132cd1b2a9ff8374e42ba03d39d076594fbb94f3e0e2c226c62/ty-0.0.16-py3-none-win_amd64.whl", hash = "sha256:f572c216aa8ecf79e86589c6e6d4bebc01f1f3cb3be765c0febd942013e1e73a", size = 10436043, upload-time = "2026-02-10T20:23:57.51Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/02/70a491802e7593e444137ed4e41a04c34d186eb2856f452dd76b60f2e325/ty-0.0.16-py3-none-win_arm64.whl", hash = "sha256:430eadeb1c0de0c31ef7bef9d002bdbb5f25a31e3aad546f1714d76cd8da0a87", size = 9915122, upload-time = "2026-02-10T20:24:14.285Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
|||
Loading…
Reference in a new issue