Add AGUI_STATE_KEY for namespaced AG-UI state emission
This commit is contained in:
parent
157773ef15
commit
7639c2915e
8 changed files with 87 additions and 17 deletions
|
|
@ -1,6 +1,12 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **AGUI_STATE_KEY Constant**: Exported `AGUI_STATE_KEY` (`"haiku.rag.chat"`) from `haiku.rag.agents.chat` for namespaced AG-UI state emission
|
||||||
|
- Enables integrators to use a consistent key when combining haiku.rag with other agents
|
||||||
|
- Backend, TUI, and frontend now use this key for state emission and extraction
|
||||||
|
|
||||||
## [0.26.3] - 2026-01-15
|
## [0.26.3] - 2026-01-15
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ from starlette.responses import JSONResponse, Response, StreamingResponse
|
||||||
from starlette.routing import Route
|
from starlette.routing import Route
|
||||||
|
|
||||||
from haiku.rag.agents.chat import (
|
from haiku.rag.agents.chat import (
|
||||||
|
AGUI_STATE_KEY,
|
||||||
ChatDeps,
|
ChatDeps,
|
||||||
ChatSessionState,
|
ChatSessionState,
|
||||||
QAResponse,
|
QAResponse,
|
||||||
|
|
@ -79,11 +80,15 @@ 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
|
# Restore qa_history from incoming state (look under namespaced key)
|
||||||
initial_qa_history: list[QAResponse] = []
|
initial_qa_history: list[QAResponse] = []
|
||||||
state = getattr(run_input, "state", None)
|
state = getattr(run_input, "state", None)
|
||||||
if state and "qa_history" in state:
|
if state:
|
||||||
initial_qa_history = [QAResponse(**qa) for qa in state.get("qa_history", [])]
|
chat_state = state.get(AGUI_STATE_KEY, state)
|
||||||
|
if "qa_history" in chat_state:
|
||||||
|
initial_qa_history = [
|
||||||
|
QAResponse(**qa) for qa in chat_state.get("qa_history", [])
|
||||||
|
]
|
||||||
|
|
||||||
# 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)
|
||||||
|
|
@ -94,6 +99,7 @@ async def stream_chat(request: Request) -> Response:
|
||||||
session_id=thread_id or "",
|
session_id=thread_id or "",
|
||||||
qa_history=initial_qa_history,
|
qa_history=initial_qa_history,
|
||||||
),
|
),
|
||||||
|
state_key=AGUI_STATE_KEY,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Use AGUIAdapter for streaming
|
# Use AGUIAdapter for streaming
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,9 @@ import "@copilotkit/react-ui/styles.css";
|
||||||
import CitationBlock from "./CitationBlock";
|
import CitationBlock from "./CitationBlock";
|
||||||
import DbInfo from "./DbInfo";
|
import DbInfo from "./DbInfo";
|
||||||
|
|
||||||
|
// Must match AGUI_STATE_KEY from haiku.rag.agents.chat
|
||||||
|
const AGUI_STATE_KEY = "haiku.rag.chat";
|
||||||
|
|
||||||
interface Citation {
|
interface Citation {
|
||||||
index: number;
|
index: number;
|
||||||
document_id: string;
|
document_id: string;
|
||||||
|
|
@ -35,6 +38,11 @@ interface ChatSessionState {
|
||||||
qa_history: QAResponse[];
|
qa_history: QAResponse[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AG-UI state is namespaced under AGUI_STATE_KEY
|
||||||
|
interface AgentState {
|
||||||
|
[AGUI_STATE_KEY]?: ChatSessionState;
|
||||||
|
}
|
||||||
|
|
||||||
function SpinnerIcon() {
|
function SpinnerIcon() {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
|
|
@ -329,20 +337,23 @@ function ToolCallIndicator({
|
||||||
}
|
}
|
||||||
|
|
||||||
function ChatContent() {
|
function ChatContent() {
|
||||||
useCoAgent<ChatSessionState>({
|
useCoAgent<AgentState>({
|
||||||
name: "chat_agent",
|
name: "chat_agent",
|
||||||
initialState: {
|
initialState: {
|
||||||
session_id: "",
|
[AGUI_STATE_KEY]: {
|
||||||
citations: [],
|
session_id: "",
|
||||||
qa_history: [],
|
citations: [],
|
||||||
|
qa_history: [],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
useCoAgentStateRender<ChatSessionState>({
|
useCoAgentStateRender<AgentState>({
|
||||||
name: "chat_agent",
|
name: "chat_agent",
|
||||||
render: ({ state }) => {
|
render: ({ state }) => {
|
||||||
if (state.citations && state.citations.length > 0) {
|
const chatState = state[AGUI_STATE_KEY];
|
||||||
return <CitationBlock citations={state.citations} />;
|
if (chatState?.citations.length) {
|
||||||
|
return <CitationBlock citations={chatState.citations} />;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -111,6 +111,37 @@ Q/A history is used to:
|
||||||
2. Avoid repeating previous answers
|
2. Avoid repeating previous answers
|
||||||
3. Enable semantic ranking of relevant past answers
|
3. Enable semantic ranking of relevant past answers
|
||||||
|
|
||||||
|
### AG-UI Integration
|
||||||
|
|
||||||
|
When using the chat agent with AG-UI streaming, state is emitted under a namespaced key to avoid conflicts with other agents:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from haiku.rag.agents.chat import AGUI_STATE_KEY, ChatDeps, ChatSessionState
|
||||||
|
|
||||||
|
# AGUI_STATE_KEY = "haiku.rag.chat"
|
||||||
|
|
||||||
|
deps = ChatDeps(
|
||||||
|
client=client,
|
||||||
|
config=config,
|
||||||
|
session_state=ChatSessionState(),
|
||||||
|
state_key=AGUI_STATE_KEY, # Enables namespaced state emission
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
The emitted state structure:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"haiku.rag.chat": {
|
||||||
|
"session_id": "",
|
||||||
|
"citations": [...],
|
||||||
|
"qa_history": [...]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Frontend clients should extract state from under this key. See the [Conversational RAG App](apps.md#conversational-rag-app) for a complete implementation example.
|
||||||
|
|
||||||
## Research Graph
|
## Research Graph
|
||||||
|
|
||||||
The research workflow is implemented as a typed pydantic-graph. It plans, searches (in parallel batches), evaluates, and synthesizes into a final report.
|
The research workflow is implemented as a typed pydantic-graph. It plans, searches (in parallel batches), evaluates, and synthesizes into a final report.
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
from haiku.rag.agents.chat.agent import create_chat_agent
|
from haiku.rag.agents.chat.agent import create_chat_agent
|
||||||
from haiku.rag.agents.chat.search import SearchAgent
|
from haiku.rag.agents.chat.search import SearchAgent
|
||||||
from haiku.rag.agents.chat.state import (
|
from haiku.rag.agents.chat.state import (
|
||||||
|
AGUI_STATE_KEY,
|
||||||
ChatDeps,
|
ChatDeps,
|
||||||
ChatSessionState,
|
ChatSessionState,
|
||||||
CitationInfo,
|
CitationInfo,
|
||||||
|
|
@ -11,6 +12,7 @@ from haiku.rag.agents.chat.state import (
|
||||||
)
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
"AGUI_STATE_KEY",
|
||||||
"create_chat_agent",
|
"create_chat_agent",
|
||||||
"SearchAgent",
|
"SearchAgent",
|
||||||
"ChatDeps",
|
"ChatDeps",
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,8 @@ if TYPE_CHECKING:
|
||||||
|
|
||||||
MAX_QA_HISTORY = 50
|
MAX_QA_HISTORY = 50
|
||||||
|
|
||||||
|
AGUI_STATE_KEY = "haiku.rag.chat"
|
||||||
|
|
||||||
_embedding_cache: dict[str, list[float]] = {}
|
_embedding_cache: dict[str, list[float]] = {}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,12 @@ from pydantic_ai import (
|
||||||
from pydantic_ai.messages import ModelMessage
|
from pydantic_ai.messages import ModelMessage
|
||||||
|
|
||||||
from haiku.rag.agents.chat.agent import create_chat_agent
|
from haiku.rag.agents.chat.agent import create_chat_agent
|
||||||
from haiku.rag.agents.chat.state import ChatDeps, ChatSessionState, CitationInfo
|
from haiku.rag.agents.chat.state import (
|
||||||
|
AGUI_STATE_KEY,
|
||||||
|
ChatDeps,
|
||||||
|
ChatSessionState,
|
||||||
|
CitationInfo,
|
||||||
|
)
|
||||||
from haiku.rag.client import HaikuRAG
|
from haiku.rag.client import HaikuRAG
|
||||||
from haiku.rag.config import get_config
|
from haiku.rag.config import get_config
|
||||||
|
|
||||||
|
|
@ -153,10 +158,10 @@ class ChatApp(App): # type: ignore[misc]
|
||||||
and meta_event.type == EventType.STATE_SNAPSHOT
|
and meta_event.type == EventType.STATE_SNAPSHOT
|
||||||
):
|
):
|
||||||
snapshot = getattr(meta_event, "snapshot", {})
|
snapshot = getattr(meta_event, "snapshot", {})
|
||||||
if "citations" in snapshot:
|
chat_state = snapshot.get(AGUI_STATE_KEY, snapshot)
|
||||||
self._last_citations = [
|
self._last_citations = [
|
||||||
CitationInfo(**c) for c in snapshot["citations"]
|
CitationInfo(**c) for c in chat_state["citations"]
|
||||||
]
|
]
|
||||||
|
|
||||||
async def _event_stream_handler(
|
async def _event_stream_handler(
|
||||||
self,
|
self,
|
||||||
|
|
@ -212,6 +217,7 @@ class ChatApp(App): # type: ignore[misc]
|
||||||
client=self.client,
|
client=self.client,
|
||||||
config=self.config,
|
config=self.config,
|
||||||
session_state=self.session_state,
|
session_state=self.session_state,
|
||||||
|
state_key=AGUI_STATE_KEY,
|
||||||
)
|
)
|
||||||
|
|
||||||
async with self.agent.run_stream(
|
async with self.agent.run_stream(
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ from pathlib import Path
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from haiku.rag.agents.chat import (
|
from haiku.rag.agents.chat import (
|
||||||
|
AGUI_STATE_KEY,
|
||||||
ChatDeps,
|
ChatDeps,
|
||||||
ChatSessionState,
|
ChatSessionState,
|
||||||
CitationInfo,
|
CitationInfo,
|
||||||
|
|
@ -40,6 +41,11 @@ def test_chat_deps_initialization(temp_db_path):
|
||||||
client.close()
|
client.close()
|
||||||
|
|
||||||
|
|
||||||
|
def test_agui_state_key_constant():
|
||||||
|
"""Test AGUI_STATE_KEY is exported with correct value."""
|
||||||
|
assert AGUI_STATE_KEY == "haiku.rag.chat"
|
||||||
|
|
||||||
|
|
||||||
def test_chat_deps_with_state_key(temp_db_path):
|
def test_chat_deps_with_state_key(temp_db_path):
|
||||||
"""Test ChatDeps can be initialized with state_key for keyed state emission."""
|
"""Test ChatDeps can be initialized with state_key for keyed state emission."""
|
||||||
client = HaikuRAG(temp_db_path, create=True)
|
client = HaikuRAG(temp_db_path, create=True)
|
||||||
|
|
@ -309,7 +315,7 @@ async def test_chat_agent_search_with_state_key(allow_model_requests, temp_db_pa
|
||||||
client=client,
|
client=client,
|
||||||
config=Config,
|
config=Config,
|
||||||
session_state=session_state,
|
session_state=session_state,
|
||||||
state_key="haiku_rag",
|
state_key=AGUI_STATE_KEY,
|
||||||
)
|
)
|
||||||
|
|
||||||
result = await agent.run(
|
result = await agent.run(
|
||||||
|
|
@ -554,7 +560,7 @@ async def test_chat_agent_ask_with_state_key(allow_model_requests, temp_db_path)
|
||||||
client=client,
|
client=client,
|
||||||
config=Config,
|
config=Config,
|
||||||
session_state=session_state,
|
session_state=session_state,
|
||||||
state_key="haiku_rag",
|
state_key=AGUI_STATE_KEY,
|
||||||
)
|
)
|
||||||
|
|
||||||
result = await agent.run(
|
result = await agent.run(
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue