Merge pull request #134 from ggozad/chore/a2a-removal

Remove A2A from haiku.rag, turn it into an example
This commit is contained in:
Yiorgis Gozadinos 2025-11-07 13:34:36 +02:00 committed by GitHub
commit 9f68349ea4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
41 changed files with 4687 additions and 321 deletions

View file

@ -5,12 +5,13 @@
- **Configuration**: All CLI commands now properly support `--config` parameter for specifying custom configuration files
- Configuration loading consolidated across CLI, app, and client with consistent resolution order
- `HaikuRAGApp`, MCP server, and A2A server now accept `config` parameter for programmatic configuration
- `HaikuRAGApp` and MCP server now accept `config` parameter for programmatic configuration
- Updated CLI documentation to clarify global vs per-command options
- **BREAKING**: Standardized configuration filename to `haiku.rag.yaml` in user directories (was incorrectly using `config.yaml`). Users with existing `config.yaml` in their user directory will need to rename it to `haiku.rag.yaml`
### Removed
- **BREAKING**: A2A (Agent-to-Agent) protocol support has been moved to a separate self-contained package in `examples/a2a-server/`. The A2A server is no longer part of the main haiku.rag package. Users who need A2A functionality can install and run it from the examples directory with `cd examples/a2a-server && uv sync`.
- **BREAKING**: Removed deprecated `.env`-based configuration system. The `haiku-rag init-config --from-env` command and `load_config_from_env()` function have been removed. All configuration must now be done via YAML files. Environment variables for API keys (e.g., `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`) and service URLs (e.g., `OLLAMA_BASE_URL`) are still supported and can be set via `.env` files.
## [0.14.1] - 2025-11-06

View file

@ -16,7 +16,6 @@ Retrieval-Augmented Generation (RAG) library built on LanceDB.
- **File monitoring**: Auto-index files when run as server
- **40+ file formats**: PDF, DOCX, HTML, Markdown, code files, URLs
- **MCP server**: Expose as tools for AI assistants
- **A2A agent**: Conversational agent with context and multi-turn dialogue
- **CLI & Python API**: Use from command line or Python
## Installation
@ -29,7 +28,7 @@ Retrieval-Augmented Generation (RAG) library built on LanceDB.
uv pip install haiku.rag
```
Includes all features: document processing, all embedding providers, rerankers, and A2A agent support.
Includes all features: document processing, all embedding providers, and rerankers.
### Slim Package (Minimal Dependencies)
@ -148,32 +147,13 @@ haiku-rag serve --stdio
Provides tools for document management and search directly in your AI assistant.
## A2A Agent
Run as a conversational agent with the Agent-to-Agent protocol:
```bash
# Start the A2A server
haiku-rag serve --a2a
# Connect with the interactive client (in another terminal)
haiku-rag a2aclient
```
The A2A agent provides:
- Multi-turn dialogue with context
- Intelligent multi-search for complex questions
- Source citations with titles and URIs
- Full document retrieval on request
## Examples
See the [examples directory](examples/) for working examples:
- **[Interactive Research Assistant](examples/ag-ui-research/)** - Full-stack research assistant with Pydantic AI and AG-UI featuring human-in-the-loop approval and real-time state synchronization
- **[Docker Setup](examples/docker/)** - Complete Docker deployment with file monitoring, MCP server, and A2A agent
- **[A2A Security](examples/a2a-security/)** - Authentication examples (API key, OAuth2, GitHub)
- **[Docker Setup](examples/docker/)** - Complete Docker deployment with file monitoring and MCP server
- **[A2A Server](examples/a2a-server/)** - Self-contained A2A protocol server package with conversational agent interface
## Documentation
@ -185,7 +165,6 @@ Full documentation at: https://ggozad.github.io/haiku.rag/
- [Python API](https://ggozad.github.io/haiku.rag/python/) - Complete API docs
- [Agents](https://ggozad.github.io/haiku.rag/agents/) - QA agent and multi-agent research
- [MCP Server](https://ggozad.github.io/haiku.rag/mcp/) - Model Context Protocol integration
- [A2A Agent](https://ggozad.github.io/haiku.rag/a2a/) - Agent-to-Agent protocol support
- [Benchmarks](https://ggozad.github.io/haiku.rag/benchmarks/) - Performance Benchmarks
mcp-name: io.github.ggozad/haiku-rag

View file

@ -33,8 +33,8 @@ ENV DEFAULT_DATA_DIR=/data
ENV PATH="/app/.venv/bin:$PATH"
# Expose ports for MCP and A2A
EXPOSE 8000 8001
# Expose port for MCP server
EXPOSE 8001
# Run all services (monitoring, MCP, A2A)
CMD ["python", "-m", "haiku.rag.cli", "serve", "--monitor", "--mcp", "--mcp-port", "8001", "--a2a", "--a2a-host", "0.0.0.0", "--a2a-port", "8000", "--db", "/data/haiku.rag.lancedb"]
# Run all services (monitoring, MCP)
CMD ["python", "-m", "haiku.rag.cli", "serve", "--monitor", "--mcp", "--mcp-port", "8001", "--db", "/data/haiku.rag.lancedb"]

View file

@ -1,6 +1,6 @@
# haiku.rag Docker Image
Pre-built images are available at `ghcr.io/ggozad/haiku.rag` with all extras (voyageai, mxbai, a2a).
Pre-built images are available at `ghcr.io/ggozad/haiku.rag` with all extras (voyageai, mxbai).
## Using Pre-built Image
@ -33,7 +33,7 @@ See [Configuration docs](https://ggozad.github.io/haiku.rag/configuration/) for
Mount your config file and data directory:
```bash
docker run -p 8000:8000 -p 8001:8001 \
docker run -p 8001:8001 \
-v $(pwd)/haiku.rag.yaml:/app/haiku.rag.yaml \
-v $(pwd)/data:/data \
ghcr.io/ggozad/haiku.rag:latest
@ -44,7 +44,7 @@ The container will automatically use the mounted `haiku.rag.yaml` configuration
For API keys (OpenAI, Anthropic, etc.), pass them as environment variables:
```bash
docker run -p 8000:8000 -p 8001:8001 \
docker run -p 8001:8001 \
-v $(pwd)/haiku.rag.yaml:/app/haiku.rag.yaml \
-v $(pwd)/data:/data \
-e OPENAI_API_KEY=your-key-here \

View file

@ -171,41 +171,18 @@ haiku-rag serve --mcp
# MCP server (stdio transport)
haiku-rag serve --mcp --stdio
# A2A server only
haiku-rag serve --a2a
# File monitoring only
haiku-rag serve --monitor
# All services
haiku-rag serve --monitor --mcp --a2a
# Both services
haiku-rag serve --monitor --mcp
# Custom ports
haiku-rag serve --mcp --mcp-port 9000 --a2a --a2a-port 9001
# Custom port
haiku-rag serve --mcp --mcp-port 9000
```
See [Server Mode](server.md) for details on available services.
### A2A Interactive Client
Connect to and chat with haiku.rag's A2A server:
```bash
# Connect to local server
haiku-rag a2aclient
# Connect to remote server
haiku-rag a2aclient --url https://example.com:8000
```
The interactive client provides:
- Rich markdown rendering of agent responses
- Multi-turn conversation with context
- Agent card discovery and display
- Compact artifact summaries
See [A2A documentation](a2a.md) for more details.
## Settings
View current configuration settings:

View file

@ -98,9 +98,6 @@ providers:
rerank_base_url: ""
qa_base_url: ""
research_base_url: ""
a2a:
max_contexts: 1000
```
## Programmatic Configuration

View file

@ -12,7 +12,6 @@
- **File monitoring**: Automatically index files when run as a server
- **Extended file format support**: Parse 40+ file formats including PDF, DOCX, HTML, Markdown, code files and more. Or add a URL!
- **MCP server**: Exposes functionality as MCP tools
- **A2A agent**: Conversational agent with context and multi-turn dialogue support
- **CLI commands**: Access all functionality from your terminal
- Add sources from text, files, or URLs, optionally with a humanreadable title
- **Python client**: Call `haiku.rag` from your own python applications
@ -59,7 +58,6 @@ haiku-rag ask "Who is the author of haiku.rag?"
- [CLI](cli.md) - Command line interface usage
- [Server](server.md) - File monitoring and server mode
- [MCP](mcp.md) - Model Context Protocol integration
- [A2A](a2a.md) - Agent-to-Agent conversational protocol
- [Python](python.md) - Python API reference
- [Agents](agents.md) - QA agent and multi-agent research

View file

@ -14,7 +14,6 @@ The full package includes **all features and extras**:
- **Document processing** (Docling) - PDF, DOCX, PPTX, images, and 40+ file formats
- **All embedding providers** - VoyageAI
- **All rerankers** - MixedBread AI, Cohere, Zero Entropy
- **A2A agent** - Agent-to-Agent protocol support
This is the easiest way to get started with all features enabled.
@ -36,7 +35,6 @@ The slim package has minimal dependencies and lets you install only what you nee
- `docling` - PDF, DOCX, PPTX, images, and other document formats
- `voyageai` - VoyageAI embeddings
- `mxbai` - MixedBread AI reranking
- `a2a` - Agent-to-Agent protocol support
- `cohere` - Cohere reranking
- `zeroentropy` - Zero Entropy reranking
@ -74,4 +72,4 @@ Run the container with all services:
docker run -p 8000:8000 -p 8001:8001 -v $(pwd)/data:/data ghcr.io/ggozad/haiku.rag:latest
```
This starts the MCP server on port 8001 and A2A server on port 8000, with data persisted to `./data`.
This starts the MCP server on port 8001, with data persisted to `./data`.

View file

@ -1,10 +1,10 @@
# Server Mode
The server provides automatic file monitoring, MCP functionality, and A2A agent support.
The server provides automatic file monitoring and MCP functionality.
## Starting the Server
The `serve` command requires at least one service flag. You can enable file monitoring, MCP server, A2A server, or any combination:
The `serve` command requires at least one service flag. You can enable file monitoring, MCP server, or both:
### MCP Server Only
@ -17,31 +17,19 @@ Transport options:
- `--stdio` - Standard input/output transport
- `--mcp-port` - Custom port (default: 8001)
### A2A Server Only
```bash
haiku-rag serve --a2a
```
Options:
- `--a2a-host` - Host to bind to (default: 127.0.0.1)
- `--a2a-port` - Port to bind to (default: 8000)
See [A2A documentation](a2a.md) for details on the conversational agent.
### File Monitoring Only
```bash
haiku-rag serve --monitor
```
### All Services
### Both Services
```bash
haiku-rag serve --monitor --mcp --a2a
haiku-rag serve --monitor --mcp
```
This will start file monitoring, MCP server on port 8001, and A2A server on port 8000.
This will start file monitoring and MCP server on port 8001.
## File Monitoring

View file

@ -22,44 +22,25 @@ See `ag-ui-research/README.md` for setup instructions.
Complete Docker setup for running haiku.rag with all services:
- File monitoring for automatic document indexing
- MCP server for AI assistant integration
- A2A agent for conversational interactions
See `docker/README.md` for setup instructions.
## A2A Security Examples
## A2A Server
**Directory:** `a2a-security/`
**Directory:** `a2a-server/`
Three examples showing how to add authentication to haiku.rag's A2A server:
Self-contained A2A (Agent-to-Agent) protocol server package that provides a conversational agent interface with its own CLI and dependencies.
### API Key Authentication
Features:
- Conversational context with multi-turn dialogue support
- Interactive CLI client for testing
- Security examples (API key, OAuth2 with GitHub, enterprise OAuth2)
- Full documentation and installation instructions
**File:** `a2a-security/apikey_example.py`
Simple header-based authentication suitable for internal services and development.
See `a2a-server/README.md` for complete setup and usage instructions.
Install locally:
```bash
python examples/a2a-security/apikey_example.py /path/to/database.lancedb
cd a2a-server
uv sync
```
### OAuth2 GitHub Authentication
**File:** `a2a-security/oauth2_github.py`
GitHub Personal Access Token authentication for GitHub-integrated services.
```bash
python examples/a2a-security/oauth2_github.py /path/to/database.lancedb
```
### OAuth2 Enterprise Authentication
**File:** `a2a-security/oauth2_example.py`
Full OAuth2 with JWT verification for enterprise environments.
```bash
python examples/a2a-security/oauth2_example.py /path/to/database.lancedb
```
See individual files for detailed setup instructions and usage examples.

View file

@ -0,0 +1,117 @@
# haiku-rag-a2a
A2A (Agent-to-Agent) protocol server for haiku.rag. This package provides a conversational agent interface that maintains conversation history and context across multiple turns.
## Features
- **Conversational Context**: Maintains full conversation history including tool calls and results
- **Multi-turn Dialogue**: Supports follow-up questions with pronoun resolution ("he", "it", "that document")
- **Intelligent Search**: Performs single or multiple searches depending on question complexity
- **Source Citations**: Always includes sources with both titles and URIs
- **Full Document Retrieval**: Can fetch complete documents on request
- **Multiple Skills**: Exposes three distinct skills with appropriate artifacts:
- `document-qa`: Conversational question answering (default)
- `document-search`: Semantic search with structured results
- `document-retrieve`: Fetch complete documents by URI
## Installation
This package is not published to PyPI. Install it locally from the haiku.rag repository:
```bash
cd examples/a2a-server
uv sync
```
This will install the package and all its dependencies, including `haiku.rag`.
## Quick Start
### Starting the A2A Server
```bash
# Start server with default database location (uses the same default as haiku-rag)
uv run haiku-rag-a2a serve
# Or specify a custom database path
uv run haiku-rag-a2a serve --db /path/to/database
# Start on custom host/port
uv run haiku-rag-a2a serve --host 0.0.0.0 --port 8080
```
By default, the server uses the same database location as `haiku-rag`:
- Linux: `~/.local/share/haiku.rag`
- macOS: `~/Library/Application Support/haiku.rag`
- Windows: `C:/Users/<USER>/AppData/Roaming/haiku.rag`
### Interactive Client
Test and interact with the A2A server using the built-in interactive client:
```bash
# Connect to local server
uv run haiku-rag-a2a client
# Connect to remote server
uv run haiku-rag-a2a client --url https://example.com:8000
```
The interactive client provides:
- Rich markdown rendering of agent responses
- Conversation context across multiple turns
- Agent card discovery and display
- Compact artifact summaries
## Python Usage
```python
from pathlib import Path
from haiku_rag_a2a.a2a import create_a2a_app
import uvicorn
# Create A2A app
app = create_a2a_app(Path("/path/to/database"))
# Run with uvicorn
uvicorn.run(app, host="127.0.0.1", port=8000)
```
## Security Examples
The `security_examples/` directory contains examples for securing the A2A server:
- `apikey_example.py` - Simple API key authentication
- `oauth2_github.py` - GitHub Personal Access Token authentication
- `oauth2_example.py` - Full OAuth2 with JWT verification
## Architecture
The A2A agent uses:
- **FastA2A**: Python framework implementing the A2A protocol
- **Pydantic AI**: Agent framework with tool support
- **In-Memory Storage**: Context and message history storage (persists during server lifetime)
- **Conversation State**: Full pydantic-ai message history serialized in A2A context
## Configuration
The server uses the same configuration as haiku.rag. You can specify a config file:
```bash
uv run haiku-rag-a2a serve --db /path/to/database --config haiku.rag.yaml
```
You can also control the maximum number of conversation contexts via the `--max-contexts` parameter (defaults to 1000).
## Documentation
See [a2a.md](./a2a.md) for detailed documentation including:
- API examples
- Security configuration
- Docker deployment
- Artifact specification
## License
MIT

View file

@ -9,7 +9,7 @@ from haiku.rag.config import AppConfig, Config
from haiku.rag.graph_common import get_model
from .context import load_message_history, save_message_history
from .models import AgentDependencies, SearchResult
from .models import A2AConfig, AgentDependencies, SearchResult
from .prompts import A2A_SYSTEM_PROMPT
from .skills import extract_question_from_task, get_agent_skills
from .storage import LRUMemoryStorage
@ -37,12 +37,14 @@ __all__ = [
"extract_question_from_task",
"get_agent_skills",
"LRUMemoryStorage",
"A2AConfig",
]
def create_a2a_app(
db_path: Path,
config: AppConfig = Config,
max_contexts: int = 1000,
security_schemes: dict | None = None,
security: list[dict[str, list[str]]] | None = None,
):
@ -51,6 +53,7 @@ def create_a2a_app(
Args:
db_path: Path to the LanceDB database
config: App configuration
max_contexts: Maximum number of conversations to keep in memory
security_schemes: Optional security scheme definitions for the AgentCard
security: Optional security requirements for the AgentCard
@ -58,9 +61,7 @@ def create_a2a_app(
A FastA2A ASGI application
"""
base_storage = InMemoryStorage()
storage = LRUMemoryStorage(
storage=base_storage, max_contexts=config.a2a.max_contexts
)
storage = LRUMemoryStorage(storage=base_storage, max_contexts=max_contexts)
broker = InMemoryBroker()
# Create the agent with native search tool
@ -123,7 +124,7 @@ def create_a2a_app(
# Create FastA2A app with custom worker lifecycle
@asynccontextmanager
async def lifespan(app):
logger.info(f"Started A2A server (max contexts: {config.a2a.max_contexts})")
logger.info(f"Started A2A server (max contexts: {max_contexts})")
async with app.task_manager:
async with worker.run():
yield

View file

@ -3,6 +3,14 @@ from pydantic import BaseModel, Field
from haiku.rag.client import HaikuRAG
class A2AConfig(BaseModel):
"""Configuration for A2A (Agent-to-Agent) protocol server."""
max_contexts: int = Field(
default=1000, description="Maximum number of conversations to keep in memory"
)
class SearchResult(BaseModel):
"""Search result with both title and URI for A2A agent."""

View file

@ -5,11 +5,11 @@ from pathlib import Path
from pydantic_ai import Agent
from haiku.rag.a2a.context import load_message_history, save_message_history
from haiku.rag.a2a.models import AgentDependencies
from haiku.rag.a2a.skills import extract_question_from_task
from haiku.rag.client import HaikuRAG
from haiku.rag.config import AppConfig, Config
from haiku_rag_a2a.a2a.context import load_message_history, save_message_history
from haiku_rag_a2a.a2a.models import AgentDependencies
from haiku_rag_a2a.a2a.skills import extract_question_from_task
try:
from fasta2a import Worker # type: ignore

View file

@ -0,0 +1,85 @@
import asyncio
import logging
from pathlib import Path
import typer
import uvicorn
from haiku.rag.config import AppConfig, Config, load_yaml_config
from haiku.rag.utils import get_default_data_dir
from haiku_rag_a2a.a2a import create_a2a_app
from haiku_rag_a2a.a2a.client import run_interactive_client
logger = logging.getLogger(__name__)
cli = typer.Typer(name="haiku-rag-a2a", no_args_is_help=True)
@cli.command("serve", help="Start haiku.rag A2A (Agent-to-Agent) server")
def serve(
db: Path | None = typer.Option(
None,
"--db",
help="Path to the database directory",
),
config_file: Path | None = typer.Option(
None,
"--config",
help="Path to the configuration file",
),
host: str = typer.Option(
"127.0.0.1",
"--host",
help="Host to bind A2A server to",
),
port: int = typer.Option(
8000,
"--port",
help="Port to bind A2A server to",
),
max_contexts: int = typer.Option(
1000,
"--max-contexts",
help="Maximum number of conversation contexts to keep in memory",
),
) -> None:
"""Start the A2A server."""
config = Config
if config_file:
yaml_data = load_yaml_config(config_file)
config = AppConfig.model_validate(yaml_data)
if db is None:
db = get_default_data_dir()
if not db.exists():
typer.echo(f"Error: Database directory {db} does not exist")
raise typer.Exit(1)
logger.info(f"Starting A2A server on {host}:{port}")
app = create_a2a_app(db_path=db, config=config, max_contexts=max_contexts)
uvicorn_config = uvicorn.Config(
app,
host=host,
port=port,
log_level="info",
)
server = uvicorn.Server(uvicorn_config)
asyncio.run(server.serve())
@cli.command("client", help="Run interactive client to chat with A2A server")
def client(
url: str = typer.Option(
"http://localhost:8000",
"--url",
help="URL of the A2A server",
),
):
"""Run the interactive A2A client."""
asyncio.run(run_interactive_client(url))
if __name__ == "__main__":
cli()

View file

@ -0,0 +1,27 @@
[project]
name = "haiku-rag-a2a"
description = "A2A protocol server for haiku.rag - Conversational agent interface"
version = "0.1.0"
authors = [{ name = "Yiorgis Gozadinos", email = "ggozadinos@gmail.com" }]
license = { text = "MIT" }
readme = "README.md"
requires-python = ">=3.12"
keywords = ["RAG", "a2a", "agent", "conversational-ai"]
dependencies = [
"haiku.rag>=0.14.0",
"fasta2a>=0.1.0",
"pydantic-ai-slim[a2a]>=1.11.1",
"rich>=14.2.0",
"httpx>=0.28.1",
]
[project.scripts]
haiku-rag-a2a = "haiku_rag_a2a.cli:cli"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["haiku_rag_a2a"]

View file

@ -22,12 +22,11 @@ Usage:
import os
from pathlib import Path
from haiku_rag_a2a.a2a import create_a2a_app
from starlette.exceptions import HTTPException
from starlette.responses import JSONResponse
from starlette.status import HTTP_401_UNAUTHORIZED
from haiku.rag.a2a import create_a2a_app
# API Key Configuration - In production, use environment variables or a secure key store
API_KEY_NAME = "X-API-Key"
VALID_API_KEY = os.getenv("API_KEY", "demo-key-12345")

View file

@ -35,6 +35,7 @@ Usage:
import os
from pathlib import Path
from haiku_rag_a2a.a2a import create_a2a_app
from jose import JWTError, jwt
from starlette.exceptions import HTTPException
from starlette.responses import JSONResponse
@ -44,8 +45,6 @@ from starlette.status import (
HTTP_500_INTERNAL_SERVER_ERROR,
)
from haiku.rag.a2a import create_a2a_app
# OAuth2 Configuration
OAUTH2_TOKEN_URL = os.getenv(
"OAUTH2_TOKEN_URL", "https://your-auth.example.com/oauth/token"

View file

@ -24,12 +24,11 @@ import os
from pathlib import Path
import httpx
from haiku_rag_a2a.a2a import create_a2a_app
from starlette.exceptions import HTTPException
from starlette.responses import JSONResponse
from starlette.status import HTTP_401_UNAUTHORIZED
from haiku.rag.a2a import create_a2a_app
# Configuration
GITHUB_API_URL = "https://api.github.com"
ALLOWED_TOKENS = (

View file

@ -5,6 +5,13 @@ import pytest
pytest.importorskip("fasta2a")
from fasta2a.schema import Message, TextPart # noqa: E402
from haiku_rag_a2a.a2a import (
extract_question_from_task,
get_agent_skills,
load_message_history,
save_message_history,
)
from haiku_rag_a2a.a2a.storage import LRUMemoryStorage
from pydantic_ai.messages import ( # noqa: E402
ModelMessage,
ModelRequest,
@ -16,13 +23,6 @@ from pydantic_ai.messages import (
TextPart as AITextPart,
)
from haiku.rag.a2a import (
extract_question_from_task,
get_agent_skills,
load_message_history,
save_message_history,
)
from haiku.rag.a2a.storage import LRUMemoryStorage
from haiku.rag.client import HaikuRAG
@ -209,7 +209,7 @@ async def test_lru_memory_storage_access_order():
@pytest.mark.asyncio
async def test_a2a_app_creation(temp_db_path):
"""Test that A2A app can be created successfully."""
from haiku.rag.a2a import create_a2a_app
from haiku_rag_a2a.a2a import create_a2a_app
# Create a test database
async with HaikuRAG(temp_db_path) as client:
@ -230,7 +230,7 @@ async def test_a2a_app_creation(temp_db_path):
@pytest.mark.asyncio
async def test_a2a_app_has_skills(temp_db_path):
"""Test that A2A app exposes skills describing its capabilities."""
from haiku.rag.a2a import create_a2a_app
from haiku_rag_a2a.a2a import create_a2a_app
# Create a test database
async with HaikuRAG(temp_db_path) as client:
@ -291,6 +291,7 @@ def test_get_agent_skills():
@pytest.mark.asyncio
async def test_build_artifacts_for_search():
"""Test that search operations produce structured search artifacts."""
from haiku_rag_a2a.a2a.worker import ConversationalWorker
from pydantic_ai.messages import (
ModelRequest,
ModelResponse,
@ -298,8 +299,6 @@ async def test_build_artifacts_for_search():
ToolReturnPart,
)
from haiku.rag.a2a.worker import ConversationalWorker
class MockResult:
output = "Found 1 relevant results:\n\n1. *Score: 0.9* | **test**\nresult"
@ -350,6 +349,7 @@ async def test_build_artifacts_for_search():
@pytest.mark.asyncio
async def test_build_artifacts_for_retrieve():
"""Test that retrieve operations produce document artifacts."""
from haiku_rag_a2a.a2a.worker import ConversationalWorker
from pydantic_ai.messages import (
ModelRequest,
ModelResponse,
@ -357,8 +357,6 @@ async def test_build_artifacts_for_retrieve():
ToolReturnPart,
)
from haiku.rag.a2a.worker import ConversationalWorker
class MockResult:
output = "Document content"
@ -407,6 +405,7 @@ async def test_build_artifacts_for_retrieve():
@pytest.mark.asyncio
async def test_build_artifacts_for_multiple_searches():
"""Test that multiple searches each get their own artifact with correct results."""
from haiku_rag_a2a.a2a.worker import ConversationalWorker
from pydantic_ai.messages import (
ModelRequest,
ModelResponse,
@ -415,8 +414,6 @@ async def test_build_artifacts_for_multiple_searches():
)
from pydantic_ai.messages import TextPart as AITextPart
from haiku.rag.a2a.worker import ConversationalWorker
class MockResult:
output = "Answer based on multiple searches"
@ -513,11 +510,10 @@ async def test_build_artifacts_for_multiple_searches():
@pytest.mark.asyncio
async def test_qa_artifact_for_conversational_messages():
"""Test that conversational Q&A messages always create qa_result artifacts."""
from haiku_rag_a2a.a2a.worker import ConversationalWorker
from pydantic_ai.messages import ModelResponse
from pydantic_ai.messages import TextPart as AITextPart
from haiku.rag.a2a.worker import ConversationalWorker
class MockResult:
output = "Hello! How can I help you?"
@ -552,6 +548,7 @@ async def test_qa_artifact_for_conversational_messages():
@pytest.mark.asyncio
async def test_build_artifacts_for_qa():
"""Test that Q&A operations produce artifacts for each tool call."""
from haiku_rag_a2a.a2a.worker import ConversationalWorker
from pydantic_ai.messages import (
ModelRequest,
ModelResponse,
@ -560,8 +557,6 @@ async def test_build_artifacts_for_qa():
)
from pydantic_ai.messages import TextPart as AITextPart
from haiku.rag.a2a.worker import ConversationalWorker
class MockResult:
output = "This is the answer"

4373
examples/a2a-server/uv.lock Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
# haiku.rag Docker Compose Example
Run haiku.rag with file monitoring, MCP server, and A2A agent.
Run haiku.rag with file monitoring and MCP server.
## Quick Start
@ -23,14 +23,10 @@ docker compose exec haiku-rag haiku-rag search "your query"
# Ask questions
docker compose exec haiku-rag haiku-rag ask "What is haiku.rag?"
# A2A interactive client
docker compose exec haiku-rag haiku-rag a2aclient --url http://localhost:8000
```
## Ports
- `8000` - A2A agent
- `8001` - MCP server
## Configuration
@ -52,4 +48,3 @@ docker compose up -d
- [Configuration](https://ggozad.github.io/haiku.rag/configuration/)
- [CLI Commands](https://ggozad.github.io/haiku.rag/cli/)
- [MCP Server](https://ggozad.github.io/haiku.rag/mcp/)
- [A2A Agent](https://ggozad.github.io/haiku.rag/a2a/)

View file

@ -5,7 +5,6 @@ services:
dockerfile: docker/Dockerfile
container_name: haiku-rag
ports:
- "8000:8000" # A2A server
- "8001:8001" # MCP server
volumes:
- ./data:/data # Persist database
@ -19,9 +18,3 @@ services:
- CO_API_KEY=${CO_API_KEY}
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s

View file

@ -2,7 +2,7 @@
Retrieval-Augmented Generation (RAG) library built on LanceDB - Core package with minimal dependencies.
`haiku.rag-slim` is the core package for users who want to install only the dependencies they need. Document processing (docling), rerankers, and A2A support are all optional extras.
`haiku.rag-slim` is the core package for users who want to install only the dependencies they need. Document processing (docling), and reranker support are all optional extras.
**For most users, we recommend installing [`haiku.rag`](https://pypi.org/project/haiku.rag/) instead**, which includes all features out of the box.
@ -48,8 +48,6 @@ Adds support for 40+ file formats including PDF, DOCX, HTML, and more.
- `bedrock` - AWS Bedrock
- `vertexai` - Google Vertex AI
**Agent Protocol:**
- `a2a` - Agent-to-Agent protocol
```bash
# Common combinations
@ -64,7 +62,6 @@ See the main [`haiku.rag`](https://github.com/ggozad/haiku.rag) repository for:
- CLI examples
- Python API usage
- MCP server setup
- A2A agent configuration
## Documentation

View file

@ -455,9 +455,6 @@ class HaikuRAGApp:
enable_mcp: bool = True,
mcp_transport: str | None = None,
mcp_port: int = 8001,
enable_a2a: bool = False,
a2a_host: str = "127.0.0.1",
a2a_port: int = 8000,
):
"""Start the server with selected services."""
async with HaikuRAG(self.db_path, config=self.config) as client:
@ -485,33 +482,6 @@ class HaikuRAGApp:
mcp_task = asyncio.create_task(run_mcp())
tasks.append(mcp_task)
# Start A2A server if enabled
if enable_a2a:
try:
from haiku.rag.a2a import create_a2a_app
except ImportError as e:
logger.error(f"Failed to import A2A: {e}")
return
import uvicorn
logger.info(f"Starting A2A server on {a2a_host}:{a2a_port}")
async def run_a2a():
app = create_a2a_app(db_path=self.db_path, config=self.config)
uvicorn_config = uvicorn.Config(
app,
host=a2a_host,
port=a2a_port,
log_level="warning",
access_log=False,
)
server = uvicorn.Server(uvicorn_config)
await server.serve()
a2a_task = asyncio.create_task(run_a2a())
tasks.append(a2a_task)
if not tasks:
logger.warning("No services enabled")
return

View file

@ -401,7 +401,7 @@ def download_models_cmd():
@cli.command(
"serve",
help="Start haiku.rag server. Use --monitor, --mcp, and/or --a2a to enable services.",
help="Start haiku.rag server. Use --monitor and/or --mcp to enable services.",
)
def serve(
db: Path | None = typer.Option(
@ -429,27 +429,12 @@ def serve(
"--mcp-port",
help="Port to bind MCP server to (ignored with --stdio)",
),
a2a: bool = typer.Option(
False,
"--a2a",
help="Enable A2A (Agent-to-Agent) server",
),
a2a_host: str = typer.Option(
"127.0.0.1",
"--a2a-host",
help="Host to bind A2A server to",
),
a2a_port: int = typer.Option(
8000,
"--a2a-port",
help="Port to bind A2A server to",
),
) -> None:
"""Start the server with selected services."""
# Require at least one service flag
if not (monitor or mcp or a2a):
if not (monitor or mcp):
typer.echo(
"Error: At least one service flag (--monitor, --mcp, or --a2a) must be specified"
"Error: At least one service flag (--monitor or --mcp) must be specified"
)
raise typer.Exit(1)
@ -467,34 +452,9 @@ def serve(
enable_mcp=mcp,
mcp_transport=transport,
mcp_port=mcp_port,
enable_a2a=a2a,
a2a_host=a2a_host,
a2a_port=a2a_port,
)
)
@cli.command(
"a2aclient", help="Run interactive client to chat with haiku.rag's A2A server"
)
def a2aclient(
url: str = typer.Option(
"http://localhost:8000",
"--url",
help="Base URL of the A2A server",
),
):
try:
from haiku.rag.a2a.client import run_interactive_client
except ImportError:
typer.echo(
"Error: A2A support requires the 'a2a' extra. "
"Install with: uv pip install 'haiku.rag[a2a]'"
)
raise typer.Exit(1)
asyncio.run(run_interactive_client(url=url))
if __name__ == "__main__":
cli()

View file

@ -6,7 +6,6 @@ from haiku.rag.config.loader import (
load_yaml_config,
)
from haiku.rag.config.models import (
A2AConfig,
AppConfig,
EmbeddingsConfig,
LanceDBConfig,
@ -35,7 +34,6 @@ __all__ = [
"OllamaConfig",
"VLLMConfig",
"ProvidersConfig",
"A2AConfig",
"find_config_file",
"load_yaml_config",
"generate_default_config",

View file

@ -84,5 +84,4 @@ def generate_default_config() -> dict:
"research_base_url": "",
},
},
"a2a": {"max_contexts": 1000},
}

View file

@ -76,10 +76,6 @@ class ProvidersConfig(BaseModel):
vllm: VLLMConfig = Field(default_factory=VLLMConfig)
class A2AConfig(BaseModel):
max_contexts: int = 1000
class AppConfig(BaseModel):
environment: str = "production"
storage: StorageConfig = Field(default_factory=StorageConfig)
@ -91,4 +87,3 @@ class AppConfig(BaseModel):
research: ResearchConfig = Field(default_factory=ResearchConfig)
processing: ProcessingConfig = Field(default_factory=ProcessingConfig)
providers: ProvidersConfig = Field(default_factory=ProvidersConfig)
a2a: A2AConfig = Field(default_factory=A2AConfig)

View file

@ -45,8 +45,6 @@ voyageai = ["voyageai>=0.3.5"]
mxbai = ["mxbai-rerank>=0.1.6"]
cohere = ["cohere>=5.0.0"]
zeroentropy = ["zeroentropy>=0.1.0a6"]
# Agent protocols
a2a = ["fasta2a>=0.1.0", "pydantic-ai-slim[a2a]"]
# Model providers (delegated to pydantic-ai-slim)
anthropic = ["pydantic-ai-slim[anthropic]"]
groq = ["pydantic-ai-slim[groq]"]

View file

@ -65,7 +65,6 @@ nav:
- Agents: agents.md
- Server: server.md
- MCP: mcp.md
- A2A: a2a.md
- Benchmarks: benchmarks.md
markdown_extensions:
- admonition

View file

@ -21,7 +21,7 @@ classifiers = [
"Typing :: Typed",
]
dependencies = ["haiku.rag-slim[docling,voyageai,mxbai,a2a,cohere,zeroentropy]"]
dependencies = ["haiku.rag-slim[docling,voyageai,mxbai,cohere,zeroentropy]"]
[project.scripts]
haiku-rag = "haiku.rag.cli:cli"
@ -100,6 +100,8 @@ pythonVersion = "3.12"
[tool.pytest.ini_options]
asyncio_default_fixture_loop_scope = "session"
asyncio_mode = "auto"
testpaths = ["tests"]
norecursedirs = ["examples", "docs", "evaluations", ".git", ".venv"]
# pyproject.toml
filterwarnings = ["error", "ignore::UserWarning", "ignore::DeprecationWarning"]

View file

@ -211,7 +211,6 @@ async def test_serve_mcp_only(app: HaikuRAGApp, monkeypatch, transport):
enable_monitor=False,
enable_mcp=True,
mcp_transport=transport,
enable_a2a=False,
)
except asyncio.CancelledError:
pass
@ -245,50 +244,16 @@ async def test_serve_monitor_only(app: HaikuRAGApp, monkeypatch):
with patch("haiku.rag.app.HaikuRAG", return_value=mock_client):
try:
await app.serve(enable_monitor=True, enable_mcp=False, enable_a2a=False)
await app.serve(enable_monitor=True, enable_mcp=False)
except asyncio.CancelledError:
pass
assert len(created_tasks) == 1
@pytest.mark.asyncio
async def test_serve_a2a_only(app: HaikuRAGApp, monkeypatch):
"""Test the serve method with A2A server only."""
pytest.importorskip("fasta2a")
created_tasks = []
original_create_task = asyncio.create_task
def track_task(coro):
task = original_create_task(coro)
created_tasks.append(task)
task.cancel()
return task
monkeypatch.setattr("haiku.rag.app.asyncio.create_task", track_task)
monkeypatch.setattr(
"haiku.rag.app.asyncio.gather", AsyncMock(side_effect=asyncio.CancelledError)
)
mock_client = AsyncMock()
mock_client.__aenter__.return_value = mock_client
mock_a2a_app = MagicMock()
with patch("haiku.rag.app.HaikuRAG", return_value=mock_client):
with patch("haiku.rag.a2a.create_a2a_app", return_value=mock_a2a_app):
try:
await app.serve(enable_monitor=False, enable_mcp=False, enable_a2a=True)
except asyncio.CancelledError:
pass
assert len(created_tasks) == 1
@pytest.mark.asyncio
async def test_serve_all_services(app: HaikuRAGApp, monkeypatch):
"""Test the serve method with all services enabled."""
pytest.importorskip("fasta2a")
created_tasks = []
original_create_task = asyncio.create_task
@ -300,7 +265,6 @@ async def test_serve_all_services(app: HaikuRAGApp, monkeypatch):
mock_server = AsyncMock()
mock_watcher = AsyncMock()
mock_a2a_app = MagicMock()
monkeypatch.setattr(
"haiku.rag.app.create_mcp_server", MagicMock(return_value=mock_server)
@ -317,13 +281,12 @@ async def test_serve_all_services(app: HaikuRAGApp, monkeypatch):
mock_client.__aenter__.return_value = mock_client
with patch("haiku.rag.app.HaikuRAG", return_value=mock_client):
with patch("haiku.rag.a2a.create_a2a_app", return_value=mock_a2a_app):
try:
await app.serve(enable_monitor=True, enable_mcp=True, enable_a2a=True)
except asyncio.CancelledError:
pass
try:
await app.serve(enable_monitor=True, enable_mcp=True)
except asyncio.CancelledError:
pass
assert len(created_tasks) == 3
assert len(created_tasks) == 2
@pytest.mark.asyncio

View file

@ -196,7 +196,6 @@ def test_serve_mcp_only():
_, kwargs = mock_app_instance.serve.call_args
assert kwargs["enable_monitor"] is False
assert kwargs["enable_mcp"] is True
assert kwargs["enable_a2a"] is False
assert kwargs["mcp_transport"] is None
assert kwargs["mcp_port"] == 8001
@ -230,26 +229,6 @@ def test_serve_monitor_only():
_, kwargs = mock_app_instance.serve.call_args
assert kwargs["enable_monitor"] is True
assert kwargs["enable_mcp"] is False
assert kwargs["enable_a2a"] is False
def test_serve_a2a_only():
"""Test serve command with A2A only."""
with patch("haiku.rag.cli.HaikuRAGApp") as mock_app:
mock_app_instance = MagicMock()
mock_app_instance.serve = AsyncMock()
mock_app.return_value = mock_app_instance
result = runner.invoke(cli, ["serve", "--a2a"])
assert result.exit_code == 0
mock_app_instance.serve.assert_called_once()
_, kwargs = mock_app_instance.serve.call_args
assert kwargs["enable_monitor"] is False
assert kwargs["enable_mcp"] is False
assert kwargs["enable_a2a"] is True
assert kwargs["a2a_host"] == "127.0.0.1"
assert kwargs["a2a_port"] == 8000
def test_serve_all_services():
@ -259,32 +238,28 @@ def test_serve_all_services():
mock_app_instance.serve = AsyncMock()
mock_app.return_value = mock_app_instance
result = runner.invoke(cli, ["serve", "--monitor", "--mcp", "--a2a"])
result = runner.invoke(cli, ["serve", "--monitor", "--mcp"])
assert result.exit_code == 0
mock_app_instance.serve.assert_called_once()
_, kwargs = mock_app_instance.serve.call_args
assert kwargs["enable_monitor"] is True
assert kwargs["enable_mcp"] is True
assert kwargs["enable_a2a"] is True
def test_serve_custom_ports():
"""Test serve command with custom ports."""
"""Test serve command with custom MCP port."""
with patch("haiku.rag.cli.HaikuRAGApp") as mock_app:
mock_app_instance = MagicMock()
mock_app_instance.serve = AsyncMock()
mock_app.return_value = mock_app_instance
result = runner.invoke(
cli, ["serve", "--mcp", "--mcp-port", "9000", "--a2a", "--a2a-port", "9001"]
)
result = runner.invoke(cli, ["serve", "--mcp", "--mcp-port", "9000"])
assert result.exit_code == 0
mock_app_instance.serve.assert_called_once()
_, kwargs = mock_app_instance.serve.call_args
assert kwargs["mcp_port"] == 9000
assert kwargs["a2a_port"] == 9001
def test_serve_stdio_without_mcp():