Pass context for CLI/app

This commit is contained in:
Yiorgis Gozadinos 2026-01-16 12:40:35 +02:00
parent d82e1f95c1
commit 18362f01c2
No known key found for this signature in database
6 changed files with 120 additions and 15 deletions

View file

@ -80,8 +80,9 @@ async def stream_chat(request: Request) -> Response:
accept = request.headers.get("accept", SSE_CONTENT_TYPE) accept = request.headers.get("accept", SSE_CONTENT_TYPE)
run_input = AGUIAdapter.build_run_input(body) run_input = AGUIAdapter.build_run_input(body)
# Restore qa_history from incoming state (look under namespaced key) # Restore session state from incoming AG-UI state (look under namespaced key)
initial_qa_history: list[QAResponse] = [] initial_qa_history: list[QAResponse] = []
initial_context: str | None = None
state = getattr(run_input, "state", None) state = getattr(run_input, "state", None)
if state: if state:
chat_state = state.get(AGUI_STATE_KEY, state) chat_state = state.get(AGUI_STATE_KEY, state)
@ -89,6 +90,7 @@ async def stream_chat(request: Request) -> Response:
initial_qa_history = [ initial_qa_history = [
QAResponse(**qa) for qa in chat_state.get("qa_history", []) QAResponse(**qa) for qa in chat_state.get("qa_history", [])
] ]
initial_context = chat_state.get("initial_context")
# Build deps with session state # Build deps with session state
thread_id = getattr(run_input, "thread_id", None) thread_id = getattr(run_input, "thread_id", None)
@ -98,6 +100,7 @@ async def stream_chat(request: Request) -> Response:
session_state=ChatSessionState( session_state=ChatSessionState(
session_id=thread_id or "", session_id=thread_id or "",
qa_history=initial_qa_history, qa_history=initial_qa_history,
initial_context=initial_context,
), ),
state_key=AGUI_STATE_KEY, state_key=AGUI_STATE_KEY,
) )

View file

@ -376,6 +376,7 @@ class HaikuRAGApp:
cite: bool = False, cite: bool = False,
deep: bool = False, deep: bool = False,
filter: str | None = None, filter: str | None = None,
initial_context: str | None = None,
): ):
"""Ask a question using the RAG system. """Ask a question using the RAG system.
@ -384,6 +385,7 @@ class HaikuRAGApp:
cite: Include citations in the answer cite: Include citations in the answer
deep: Use deep QA mode (multi-step reasoning) deep: Use deep QA mode (multi-step reasoning)
filter: SQL WHERE clause to filter documents filter: SQL WHERE clause to filter documents
initial_context: Optional background context for the question
""" """
async with HaikuRAG( async with HaikuRAG(
db_path=self.db_path, db_path=self.db_path,
@ -394,7 +396,9 @@ class HaikuRAGApp:
citations = [] citations = []
if deep: if deep:
graph = build_research_graph(config=self.config) graph = build_research_graph(config=self.config)
context = ResearchContext(original_question=question) context = ResearchContext(
original_question=question, initial_context=initial_context
)
state = ResearchState.from_config( state = ResearchState.from_config(
context=context, context=context,
config=self.config, config=self.config,
@ -423,7 +427,14 @@ class HaikuRAGApp:
else: else:
self.console.print("[yellow]No answer generated.[/yellow]") self.console.print("[yellow]No answer generated.[/yellow]")
else: else:
answer, citations = await self.client.ask(question, filter=filter) system_prompt = (
f"BACKGROUND CONTEXT:\n{initial_context}"
if initial_context
else None
)
answer, citations = await self.client.ask(
question, system_prompt=system_prompt, filter=filter
)
self.console.print(f"[bold blue]Question:[/bold blue] {question}") self.console.print(f"[bold blue]Question:[/bold blue] {question}")
self.console.print() self.console.print()
@ -433,12 +444,18 @@ class HaikuRAGApp:
for renderable in format_citations_rich(citations): for renderable in format_citations_rich(citations):
self.console.print(renderable) self.console.print(renderable)
async def research(self, question: str, filter: str | None = None): async def research(
self,
question: str,
filter: str | None = None,
initial_context: str | None = None,
):
"""Run research via the pydantic-graph pipeline. """Run research via the pydantic-graph pipeline.
Args: Args:
question: The research question question: The research question
filter: SQL WHERE clause to filter documents filter: SQL WHERE clause to filter documents
initial_context: Optional background context for the research
""" """
async with HaikuRAG( async with HaikuRAG(
db_path=self.db_path, db_path=self.db_path,
@ -451,7 +468,9 @@ class HaikuRAGApp:
self.console.print() self.console.print()
graph = build_research_graph(config=self.config) graph = build_research_graph(config=self.config)
context = ResearchContext(original_question=question) context = ResearchContext(
original_question=question, initial_context=initial_context
)
state = ResearchState.from_config(context=context, config=self.config) state = ResearchState.from_config(context=context, config=self.config)
state.search_filter = filter state.search_filter = filter
deps = ResearchDeps(client=client) deps = ResearchDeps(client=client)

View file

@ -6,6 +6,7 @@ def run_chat(
db_path: Path | None = None, db_path: Path | None = None,
read_only: bool = False, read_only: bool = False,
before: datetime | None = None, before: datetime | None = None,
initial_context: str | None = None,
) -> None: ) -> None:
"""Run the chat TUI. """Run the chat TUI.
@ -13,6 +14,7 @@ def run_chat(
db_path: Path to the LanceDB database. If None, uses default from config. db_path: Path to the LanceDB database. If None, uses default from config.
read_only: Whether to open the database in read-only mode. read_only: Whether to open the database in read-only mode.
before: Query database as it existed before this datetime. before: Query database as it existed before this datetime.
initial_context: Optional background context for the conversation.
""" """
try: try:
from haiku.rag.chat.app import ChatApp from haiku.rag.chat.app import ChatApp
@ -27,5 +29,7 @@ def run_chat(
if db_path is None: if db_path is None:
db_path = config.storage.data_dir / "haiku.rag.lancedb" db_path = config.storage.data_dir / "haiku.rag.lancedb"
app = ChatApp(db_path, read_only=read_only, before=before) app = ChatApp(
db_path, read_only=read_only, before=before, initial_context=initial_context
)
app.run() app.run()

View file

@ -85,12 +85,17 @@ class ChatApp(App): # type: ignore[misc]
] ]
def __init__( def __init__(
self, db_path: Path, read_only: bool = False, before: datetime | None = None self,
db_path: Path,
read_only: bool = False,
before: datetime | None = None,
initial_context: str | None = None,
) -> None: ) -> None:
super().__init__() super().__init__()
self.db_path = db_path self.db_path = db_path
self.read_only = read_only self.read_only = read_only
self.before = before self.before = before
self.initial_context = initial_context
self.client: HaikuRAG | None = None self.client: HaikuRAG | None = None
self.config = get_config() self.config = get_config()
self.agent: Agent[ChatDeps, str] | None = None self.agent: Agent[ChatDeps, str] | None = None
@ -121,7 +126,10 @@ class ChatApp(App): # type: ignore[misc]
# Create agent and session state # Create agent and session state
self.agent = create_chat_agent(self.config) self.agent = create_chat_agent(self.config)
self.session_state = ChatSessionState(session_id=str(uuid.uuid4())) self.session_state = ChatSessionState(
session_id=str(uuid.uuid4()),
initial_context=self.initial_context,
)
# Focus the input field # Focus the input field
self.query_one(Input).focus() self.query_one(Input).focus()
@ -266,8 +274,11 @@ class ChatApp(App): # type: ignore[misc]
self._last_citations.clear() self._last_citations.clear()
self._selected_citation_idx = None self._selected_citation_idx = None
self._message_history.clear() self._message_history.clear()
# Reset session state for fresh conversation # Reset session state for fresh conversation (preserve initial_context)
self.session_state = ChatSessionState(session_id=str(uuid.uuid4())) self.session_state = ChatSessionState(
session_id=str(uuid.uuid4()),
initial_context=self.initial_context,
)
def action_focus_input(self) -> None: def action_focus_input(self) -> None:
"""Focus the input field, or cancel if processing.""" """Focus the input field, or cancel if processing."""

View file

@ -338,9 +338,34 @@ def ask(
"-f", "-f",
help="SQL WHERE clause to filter documents (e.g., \"uri LIKE '%arxiv%'\")", help="SQL WHERE clause to filter documents (e.g., \"uri LIKE '%arxiv%'\")",
), ),
context: str | None = typer.Option(
None,
"--context",
help="Background context for the question",
),
context_file: Path | None = typer.Option(
None,
"--context-file",
help="Path to a file containing background context",
),
): ):
# Resolve initial context from flag or file
initial_context: str | None = None
if context_file:
initial_context = context_file.read_text()
elif context:
initial_context = context
app = create_app(db) app = create_app(db)
asyncio.run(app.ask(question=question, cite=cite, deep=deep, filter=filter)) asyncio.run(
app.ask(
question=question,
cite=cite,
deep=deep,
filter=filter,
initial_context=initial_context,
)
)
@cli.command("research", help="Run multi-agent research and output a concise report") @cli.command("research", help="Run multi-agent research and output a concise report")
@ -357,9 +382,28 @@ def research(
"-f", "-f",
help="SQL WHERE clause to filter documents (e.g., \"uri LIKE '%arxiv%'\")", help="SQL WHERE clause to filter documents (e.g., \"uri LIKE '%arxiv%'\")",
), ),
context: str | None = typer.Option(
None,
"--context",
help="Background context for the research",
),
context_file: Path | None = typer.Option(
None,
"--context-file",
help="Path to a file containing background context",
),
): ):
# Resolve initial context from flag or file
initial_context: str | None = None
if context_file:
initial_context = context_file.read_text()
elif context:
initial_context = context
app = create_app(db) app = create_app(db)
asyncio.run(app.research(question=question, filter=filter)) asyncio.run(
app.research(question=question, filter=filter, initial_context=initial_context)
)
@cli.command("settings", help="Display current configuration settings") @cli.command("settings", help="Display current configuration settings")
@ -547,12 +591,32 @@ def chat(
"--db", "--db",
help="Path to the LanceDB database file", help="Path to the LanceDB database file",
), ),
context: str | None = typer.Option(
None,
"--context",
help="Initial context/background information for the conversation",
),
context_file: Path | None = typer.Option(
None,
"--context-file",
help="Path to a file containing initial context",
),
): ):
"""Launch the chat TUI for conversational RAG.""" """Launch the chat TUI for conversational RAG."""
from haiku.rag.chat import run_chat from haiku.rag.chat import run_chat
db_path = db if db else get_config().storage.data_dir / "haiku.rag.lancedb" db_path = db if db else get_config().storage.data_dir / "haiku.rag.lancedb"
run_chat(db_path, read_only=_read_only, before=_before)
# Resolve initial context from flag or file
initial_context: str | None = None
if context_file:
initial_context = context_file.read_text()
elif context:
initial_context = context
run_chat(
db_path, read_only=_read_only, before=_before, initial_context=initial_context
)
@cli.command( @cli.command(

View file

@ -304,7 +304,9 @@ async def test_ask_without_cite(app: HaikuRAGApp, monkeypatch):
with patch("haiku.rag.app.HaikuRAG", return_value=mock_client): with patch("haiku.rag.app.HaikuRAG", return_value=mock_client):
await app.ask("test question") await app.ask("test question")
mock_client.ask.assert_called_once_with("test question", filter=None) mock_client.ask.assert_called_once_with(
"test question", system_prompt=None, filter=None
)
@pytest.mark.asyncio @pytest.mark.asyncio
@ -333,7 +335,9 @@ async def test_ask_with_cite(app: HaikuRAGApp, monkeypatch):
with patch("haiku.rag.app.HaikuRAG", return_value=mock_client): with patch("haiku.rag.app.HaikuRAG", return_value=mock_client):
await app.ask("test question", cite=True) await app.ask("test question", cite=True)
mock_client.ask.assert_called_once_with("test question", filter=None) mock_client.ask.assert_called_once_with(
"test question", system_prompt=None, filter=None
)
# Verify print was called (once for answer, once for citations) # Verify print was called (once for answer, once for citations)
assert mock_print.call_count >= 1 assert mock_print.call_count >= 1