diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b83dc2cd..79a9ed52 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -25,8 +25,28 @@ jobs: - name: Type check run: uv run pyright + lint-frontend: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + with: + version: 10 + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: "22" + cache: "pnpm" + cache-dependency-path: app/frontend/pnpm-lock.yaml + - name: Install dependencies + working-directory: app/frontend + run: pnpm install --frozen-lockfile + - name: Lint and format check + working-directory: app/frontend + run: pnpm run check + test: - needs: lint + needs: [lint, lint-frontend] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f9155440..46a4d4a1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,3 +20,12 @@ repos: rev: v1.1.407 hooks: - id: pyright + + - repo: local + hooks: + - id: biome + name: biome check + entry: bash -c 'cd app/frontend && npm run check' + language: system + files: ^app/frontend/ + types_or: [javascript, jsx, ts, tsx, json] diff --git a/CHANGELOG.md b/CHANGELOG.md index 0801be0d..1d037b8b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,19 @@ # Changelog ## [Unreleased] +### Added + +- **Background Context Support**: Pass background context to agents via CLI or Python API + - `haiku-rag ask --context "..." --context-file path` for Q&A with background context + - `haiku-rag research --context "..." --context-file path` for research with background context + - `haiku-rag chat --context "..." --context-file path` for chat sessions with persistent context + - `ResearchContext(background_context="...")` for Python API usage + - `ChatSessionState(background_context="...")` for chat agent sessions + - Context is included in agent system prompts and research graph planning +- **Frontend Background Context**: Settings panel in the chat app to configure persistent background context + - Context is stored in localStorage and sent with each conversation +- **Frontend Linting**: Added Biome for linting and formatting the frontend codebase + ## [0.26.4] - 2026-01-15 ### Added diff --git a/app/backend/main.py b/app/backend/main.py index 42a4fa5d..e8c4309a 100644 --- a/app/backend/main.py +++ b/app/backend/main.py @@ -80,8 +80,9 @@ async def stream_chat(request: Request) -> Response: accept = request.headers.get("accept", SSE_CONTENT_TYPE) 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] = [] + background_context: str | None = None state = getattr(run_input, "state", None) if state: chat_state = state.get(AGUI_STATE_KEY, state) @@ -89,6 +90,7 @@ async def stream_chat(request: Request) -> Response: initial_qa_history = [ QAResponse(**qa) for qa in chat_state.get("qa_history", []) ] + background_context = chat_state.get("background_context") # Build deps with session state thread_id = getattr(run_input, "thread_id", None) @@ -98,6 +100,7 @@ async def stream_chat(request: Request) -> Response: session_state=ChatSessionState( session_id=thread_id or "", qa_history=initial_qa_history, + background_context=background_context, ), state_key=AGUI_STATE_KEY, ) diff --git a/app/frontend/biome.json b/app/frontend/biome.json index 03241efc..b9fe69ab 100644 --- a/app/frontend/biome.json +++ b/app/frontend/biome.json @@ -1,9 +1,9 @@ { "$schema": "https://biomejs.dev/schemas/2.2.6/schema.json", "vcs": { - "enabled": false, + "enabled": true, "clientKind": "git", - "useIgnoreFile": false + "useIgnoreFile": true }, "files": { "ignoreUnknown": false @@ -15,7 +15,11 @@ "linter": { "enabled": true, "rules": { - "recommended": true + "recommended": true, + "a11y": { + "noAutofocus": "off", + "noSvgWithoutTitle": "off" + } } }, "javascript": { diff --git a/app/frontend/components/Chat.tsx b/app/frontend/components/Chat.tsx index dafca4a9..68b0c546 100644 --- a/app/frontend/components/Chat.tsx +++ b/app/frontend/components/Chat.tsx @@ -7,9 +7,11 @@ import { useCopilotAction, } from "@copilotkit/react-core"; import { CopilotChat } from "@copilotkit/react-ui"; +import { useCallback, useEffect, useState } from "react"; import "@copilotkit/react-ui/styles.css"; import CitationBlock from "./CitationBlock"; import DbInfo from "./DbInfo"; +import SettingsPanel, { STORAGE_KEY } from "./SettingsPanel"; // Must match AGUI_STATE_KEY from haiku.rag.agents.chat const AGUI_STATE_KEY = "haiku.rag.chat"; @@ -36,6 +38,7 @@ interface ChatSessionState { session_id: string; citations: Citation[]; qa_history: QAResponse[]; + background_context: string | null; } // AG-UI state is namespaced under AGUI_STATE_KEY @@ -131,6 +134,24 @@ function FileIcon() { ); } +function SettingsIcon() { + return ( + + + + + ); +} + function ToolCallIndicator({ toolName, status, @@ -336,7 +357,27 @@ function ToolCallIndicator({ ); } -function ChatContent() { +function ChatContentInner({ + backgroundContext, + setBackgroundContext, +}: { + backgroundContext: string; + setBackgroundContext: (value: string) => void; +}) { + const [settingsOpen, setSettingsOpen] = useState(false); + + const handleSaveContext = useCallback( + (value: string) => { + setBackgroundContext(value); + if (value) { + localStorage.setItem(STORAGE_KEY, value); + } else { + localStorage.removeItem(STORAGE_KEY); + } + }, + [setBackgroundContext], + ); + useCoAgent({ name: "chat_agent", initialState: { @@ -344,6 +385,7 @@ function ChatContent() { session_id: "", citations: [], qa_history: [], + background_context: backgroundContext || null, }, }, }); @@ -425,6 +467,40 @@ function ChatContent() { display: flex; flex-direction: column; } + .chat-header { + display: flex; + justify-content: flex-end; + padding: 0.5rem 0.75rem; + border-bottom: 1px solid #e2e8f0; + background: #f8fafc; + } + .settings-btn { + display: flex; + align-items: center; + gap: 0.375rem; + padding: 0.375rem 0.625rem; + background: white; + border: 1px solid #e2e8f0; + border-radius: 6px; + cursor: pointer; + color: #64748b; + font-size: 0.8125rem; + transition: all 0.15s; + } + .settings-btn:hover { + background: #f1f5f9; + border-color: #cbd5e1; + color: #475569; + } + .settings-btn.has-context { + background: #eff6ff; + border-color: #bfdbfe; + color: #2563eb; + } + .settings-btn.has-context:hover { + background: #dbeafe; + border-color: #93c5fd; + } .chat-content { flex: 1; min-height: 0; @@ -438,6 +514,21 @@ function ChatContent() { `}
+
+ +
+ setSettingsOpen(false)} + onSave={handleSaveContext} + currentValue={backgroundContext} + /> ); } +function ChatContent() { + const [backgroundContext, setBackgroundContext] = useState( + null, + ); + + useEffect(() => { + const stored = localStorage.getItem(STORAGE_KEY); + setBackgroundContext(stored || ""); + }, []); + + if (backgroundContext === null) { + return null; + } + + return ( + + ); +} + export default function Chat() { return ( diff --git a/app/frontend/components/SettingsPanel.tsx b/app/frontend/components/SettingsPanel.tsx new file mode 100644 index 00000000..0e3c53ed --- /dev/null +++ b/app/frontend/components/SettingsPanel.tsx @@ -0,0 +1,188 @@ +"use client"; + +import { useCallback, useEffect, useId, useState } from "react"; + +const STORAGE_KEY = "haiku.rag.settings.background_context"; + +interface SettingsPanelProps { + isOpen: boolean; + onClose: () => void; + onSave: (backgroundContext: string) => void; + currentValue: string; +} + +export default function SettingsPanel({ + isOpen, + onClose, + onSave, + currentValue, +}: SettingsPanelProps) { + const [value, setValue] = useState(currentValue); + const titleId = useId(); + + useEffect(() => { + if (isOpen) { + setValue(currentValue); + } + }, [isOpen, currentValue]); + + const handleSave = useCallback(() => { + onSave(value); + onClose(); + }, [value, onSave, onClose]); + + const handleKeyDown = useCallback( + (e: React.KeyboardEvent) => { + if (e.key === "Escape") { + onClose(); + } + }, + [onClose], + ); + + if (!isOpen) { + return null; + } + + return ( + <> + +
+ {/* biome-ignore lint/a11y/noStaticElementInteractions: modal content wrapper */} +
e.stopPropagation()} + onKeyDown={(e) => e.stopPropagation()} + > +

+ Background Context +

+

+ Provide background information that will be used throughout your + conversation. This helps the assistant understand domain-specific + context, terminology, or any relevant details about your questions. +

+