Remove A2A from haiku.rag, turn it into an example
This commit is contained in:
parent
40ecaa0e0b
commit
9ee64a33e0
41 changed files with 4687 additions and 321 deletions
|
|
@ -5,12 +5,13 @@
|
||||||
|
|
||||||
- **Configuration**: All CLI commands now properly support `--config` parameter for specifying custom configuration files
|
- **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
|
- 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
|
- 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`
|
- **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
|
### 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.
|
- **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
|
## [0.14.1] - 2025-11-06
|
||||||
|
|
|
||||||
27
README.md
27
README.md
|
|
@ -16,7 +16,6 @@ Retrieval-Augmented Generation (RAG) library built on LanceDB.
|
||||||
- **File monitoring**: Auto-index files when run as server
|
- **File monitoring**: Auto-index files when run as server
|
||||||
- **40+ file formats**: PDF, DOCX, HTML, Markdown, code files, URLs
|
- **40+ file formats**: PDF, DOCX, HTML, Markdown, code files, URLs
|
||||||
- **MCP server**: Expose as tools for AI assistants
|
- **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
|
- **CLI & Python API**: Use from command line or Python
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
@ -29,7 +28,7 @@ Retrieval-Augmented Generation (RAG) library built on LanceDB.
|
||||||
uv pip install haiku.rag
|
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)
|
### Slim Package (Minimal Dependencies)
|
||||||
|
|
||||||
|
|
@ -148,32 +147,13 @@ haiku-rag serve --stdio
|
||||||
|
|
||||||
Provides tools for document management and search directly in your AI assistant.
|
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
|
## Examples
|
||||||
|
|
||||||
See the [examples directory](examples/) for working 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
|
- **[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
|
- **[Docker Setup](examples/docker/)** - Complete Docker deployment with file monitoring and MCP server
|
||||||
- **[A2A Security](examples/a2a-security/)** - Authentication examples (API key, OAuth2, GitHub)
|
- **[A2A Server](examples/a2a-server/)** - Self-contained A2A protocol server package with conversational agent interface
|
||||||
|
|
||||||
## Documentation
|
## 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
|
- [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
|
- [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
|
- [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
|
- [Benchmarks](https://ggozad.github.io/haiku.rag/benchmarks/) - Performance Benchmarks
|
||||||
|
|
||||||
mcp-name: io.github.ggozad/haiku-rag
|
mcp-name: io.github.ggozad/haiku-rag
|
||||||
|
|
|
||||||
|
|
@ -33,8 +33,8 @@ ENV DEFAULT_DATA_DIR=/data
|
||||||
|
|
||||||
ENV PATH="/app/.venv/bin:$PATH"
|
ENV PATH="/app/.venv/bin:$PATH"
|
||||||
|
|
||||||
# Expose ports for MCP and A2A
|
# Expose port for MCP server
|
||||||
EXPOSE 8000 8001
|
EXPOSE 8001
|
||||||
|
|
||||||
# Run all services (monitoring, MCP, A2A)
|
# Run all services (monitoring, MCP)
|
||||||
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"]
|
CMD ["python", "-m", "haiku.rag.cli", "serve", "--monitor", "--mcp", "--mcp-port", "8001", "--db", "/data/haiku.rag.lancedb"]
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# haiku.rag Docker Image
|
# 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
|
## 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:
|
Mount your config file and data directory:
|
||||||
|
|
||||||
```bash
|
```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)/haiku.rag.yaml:/app/haiku.rag.yaml \
|
||||||
-v $(pwd)/data:/data \
|
-v $(pwd)/data:/data \
|
||||||
ghcr.io/ggozad/haiku.rag:latest
|
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:
|
For API keys (OpenAI, Anthropic, etc.), pass them as environment variables:
|
||||||
|
|
||||||
```bash
|
```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)/haiku.rag.yaml:/app/haiku.rag.yaml \
|
||||||
-v $(pwd)/data:/data \
|
-v $(pwd)/data:/data \
|
||||||
-e OPENAI_API_KEY=your-key-here \
|
-e OPENAI_API_KEY=your-key-here \
|
||||||
|
|
|
||||||
31
docs/cli.md
31
docs/cli.md
|
|
@ -171,41 +171,18 @@ haiku-rag serve --mcp
|
||||||
# MCP server (stdio transport)
|
# MCP server (stdio transport)
|
||||||
haiku-rag serve --mcp --stdio
|
haiku-rag serve --mcp --stdio
|
||||||
|
|
||||||
# A2A server only
|
|
||||||
haiku-rag serve --a2a
|
|
||||||
|
|
||||||
# File monitoring only
|
# File monitoring only
|
||||||
haiku-rag serve --monitor
|
haiku-rag serve --monitor
|
||||||
|
|
||||||
# All services
|
# Both services
|
||||||
haiku-rag serve --monitor --mcp --a2a
|
haiku-rag serve --monitor --mcp
|
||||||
|
|
||||||
# Custom ports
|
# Custom port
|
||||||
haiku-rag serve --mcp --mcp-port 9000 --a2a --a2a-port 9001
|
haiku-rag serve --mcp --mcp-port 9000
|
||||||
```
|
```
|
||||||
|
|
||||||
See [Server Mode](server.md) for details on available services.
|
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
|
## Settings
|
||||||
|
|
||||||
View current configuration settings:
|
View current configuration settings:
|
||||||
|
|
|
||||||
|
|
@ -98,9 +98,6 @@ providers:
|
||||||
rerank_base_url: ""
|
rerank_base_url: ""
|
||||||
qa_base_url: ""
|
qa_base_url: ""
|
||||||
research_base_url: ""
|
research_base_url: ""
|
||||||
|
|
||||||
a2a:
|
|
||||||
max_contexts: 1000
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Programmatic Configuration
|
## Programmatic Configuration
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@
|
||||||
- **File monitoring**: Automatically index files when run as a server
|
- **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!
|
- **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
|
- **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
|
- **CLI commands**: Access all functionality from your terminal
|
||||||
- Add sources from text, files, or URLs, optionally with a human‑readable title
|
- Add sources from text, files, or URLs, optionally with a human‑readable title
|
||||||
- **Python client**: Call `haiku.rag` from your own python applications
|
- **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
|
- [CLI](cli.md) - Command line interface usage
|
||||||
- [Server](server.md) - File monitoring and server mode
|
- [Server](server.md) - File monitoring and server mode
|
||||||
- [MCP](mcp.md) - Model Context Protocol integration
|
- [MCP](mcp.md) - Model Context Protocol integration
|
||||||
- [A2A](a2a.md) - Agent-to-Agent conversational protocol
|
|
||||||
- [Python](python.md) - Python API reference
|
- [Python](python.md) - Python API reference
|
||||||
- [Agents](agents.md) - QA agent and multi-agent research
|
- [Agents](agents.md) - QA agent and multi-agent research
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@ The full package includes **all features and extras**:
|
||||||
- **Document processing** (Docling) - PDF, DOCX, PPTX, images, and 40+ file formats
|
- **Document processing** (Docling) - PDF, DOCX, PPTX, images, and 40+ file formats
|
||||||
- **All embedding providers** - VoyageAI
|
- **All embedding providers** - VoyageAI
|
||||||
- **All rerankers** - MixedBread AI, Cohere, Zero Entropy
|
- **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.
|
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
|
- `docling` - PDF, DOCX, PPTX, images, and other document formats
|
||||||
- `voyageai` - VoyageAI embeddings
|
- `voyageai` - VoyageAI embeddings
|
||||||
- `mxbai` - MixedBread AI reranking
|
- `mxbai` - MixedBread AI reranking
|
||||||
- `a2a` - Agent-to-Agent protocol support
|
|
||||||
- `cohere` - Cohere reranking
|
- `cohere` - Cohere reranking
|
||||||
- `zeroentropy` - Zero Entropy 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
|
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`.
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
# Server Mode
|
# 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
|
## 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
|
### MCP Server Only
|
||||||
|
|
||||||
|
|
@ -17,31 +17,19 @@ Transport options:
|
||||||
- `--stdio` - Standard input/output transport
|
- `--stdio` - Standard input/output transport
|
||||||
- `--mcp-port` - Custom port (default: 8001)
|
- `--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
|
### File Monitoring Only
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
haiku-rag serve --monitor
|
haiku-rag serve --monitor
|
||||||
```
|
```
|
||||||
|
|
||||||
### All Services
|
### Both Services
|
||||||
|
|
||||||
```bash
|
```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
|
## File Monitoring
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,44 +22,25 @@ See `ag-ui-research/README.md` for setup instructions.
|
||||||
Complete Docker setup for running haiku.rag with all services:
|
Complete Docker setup for running haiku.rag with all services:
|
||||||
- File monitoring for automatic document indexing
|
- File monitoring for automatic document indexing
|
||||||
- MCP server for AI assistant integration
|
- MCP server for AI assistant integration
|
||||||
- A2A agent for conversational interactions
|
|
||||||
|
|
||||||
See `docker/README.md` for setup instructions.
|
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`
|
See `a2a-server/README.md` for complete setup and usage instructions.
|
||||||
|
|
||||||
Simple header-based authentication suitable for internal services and development.
|
|
||||||
|
|
||||||
|
Install locally:
|
||||||
```bash
|
```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.
|
|
||||||
|
|
|
||||||
117
examples/a2a-server/README.md
Normal file
117
examples/a2a-server/README.md
Normal 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
|
||||||
0
examples/a2a-server/haiku_rag_a2a/__init__.py
Normal file
0
examples/a2a-server/haiku_rag_a2a/__init__.py
Normal file
|
|
@ -9,7 +9,7 @@ from haiku.rag.config import AppConfig, Config
|
||||||
from haiku.rag.graph_common import get_model
|
from haiku.rag.graph_common import get_model
|
||||||
|
|
||||||
from .context import load_message_history, save_message_history
|
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 .prompts import A2A_SYSTEM_PROMPT
|
||||||
from .skills import extract_question_from_task, get_agent_skills
|
from .skills import extract_question_from_task, get_agent_skills
|
||||||
from .storage import LRUMemoryStorage
|
from .storage import LRUMemoryStorage
|
||||||
|
|
@ -37,12 +37,14 @@ __all__ = [
|
||||||
"extract_question_from_task",
|
"extract_question_from_task",
|
||||||
"get_agent_skills",
|
"get_agent_skills",
|
||||||
"LRUMemoryStorage",
|
"LRUMemoryStorage",
|
||||||
|
"A2AConfig",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def create_a2a_app(
|
def create_a2a_app(
|
||||||
db_path: Path,
|
db_path: Path,
|
||||||
config: AppConfig = Config,
|
config: AppConfig = Config,
|
||||||
|
max_contexts: int = 1000,
|
||||||
security_schemes: dict | None = None,
|
security_schemes: dict | None = None,
|
||||||
security: list[dict[str, list[str]]] | None = None,
|
security: list[dict[str, list[str]]] | None = None,
|
||||||
):
|
):
|
||||||
|
|
@ -51,6 +53,7 @@ def create_a2a_app(
|
||||||
Args:
|
Args:
|
||||||
db_path: Path to the LanceDB database
|
db_path: Path to the LanceDB database
|
||||||
config: App configuration
|
config: App configuration
|
||||||
|
max_contexts: Maximum number of conversations to keep in memory
|
||||||
security_schemes: Optional security scheme definitions for the AgentCard
|
security_schemes: Optional security scheme definitions for the AgentCard
|
||||||
security: Optional security requirements for the AgentCard
|
security: Optional security requirements for the AgentCard
|
||||||
|
|
||||||
|
|
@ -58,9 +61,7 @@ def create_a2a_app(
|
||||||
A FastA2A ASGI application
|
A FastA2A ASGI application
|
||||||
"""
|
"""
|
||||||
base_storage = InMemoryStorage()
|
base_storage = InMemoryStorage()
|
||||||
storage = LRUMemoryStorage(
|
storage = LRUMemoryStorage(storage=base_storage, max_contexts=max_contexts)
|
||||||
storage=base_storage, max_contexts=config.a2a.max_contexts
|
|
||||||
)
|
|
||||||
broker = InMemoryBroker()
|
broker = InMemoryBroker()
|
||||||
|
|
||||||
# Create the agent with native search tool
|
# Create the agent with native search tool
|
||||||
|
|
@ -123,7 +124,7 @@ def create_a2a_app(
|
||||||
# Create FastA2A app with custom worker lifecycle
|
# Create FastA2A app with custom worker lifecycle
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def lifespan(app):
|
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 app.task_manager:
|
||||||
async with worker.run():
|
async with worker.run():
|
||||||
yield
|
yield
|
||||||
|
|
@ -3,6 +3,14 @@ from pydantic import BaseModel, Field
|
||||||
from haiku.rag.client import HaikuRAG
|
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):
|
class SearchResult(BaseModel):
|
||||||
"""Search result with both title and URI for A2A agent."""
|
"""Search result with both title and URI for A2A agent."""
|
||||||
|
|
||||||
|
|
@ -5,11 +5,11 @@ from pathlib import Path
|
||||||
|
|
||||||
from pydantic_ai import Agent
|
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.client import HaikuRAG
|
||||||
from haiku.rag.config import AppConfig, Config
|
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:
|
try:
|
||||||
from fasta2a import Worker # type: ignore
|
from fasta2a import Worker # type: ignore
|
||||||
85
examples/a2a-server/haiku_rag_a2a/cli.py
Normal file
85
examples/a2a-server/haiku_rag_a2a/cli.py
Normal 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()
|
||||||
27
examples/a2a-server/pyproject.toml
Normal file
27
examples/a2a-server/pyproject.toml
Normal 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"]
|
||||||
|
|
@ -22,12 +22,11 @@ Usage:
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from haiku_rag_a2a.a2a import create_a2a_app
|
||||||
from starlette.exceptions import HTTPException
|
from starlette.exceptions import HTTPException
|
||||||
from starlette.responses import JSONResponse
|
from starlette.responses import JSONResponse
|
||||||
from starlette.status import HTTP_401_UNAUTHORIZED
|
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 Configuration - In production, use environment variables or a secure key store
|
||||||
API_KEY_NAME = "X-API-Key"
|
API_KEY_NAME = "X-API-Key"
|
||||||
VALID_API_KEY = os.getenv("API_KEY", "demo-key-12345")
|
VALID_API_KEY = os.getenv("API_KEY", "demo-key-12345")
|
||||||
|
|
@ -35,6 +35,7 @@ Usage:
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from haiku_rag_a2a.a2a import create_a2a_app
|
||||||
from jose import JWTError, jwt
|
from jose import JWTError, jwt
|
||||||
from starlette.exceptions import HTTPException
|
from starlette.exceptions import HTTPException
|
||||||
from starlette.responses import JSONResponse
|
from starlette.responses import JSONResponse
|
||||||
|
|
@ -44,8 +45,6 @@ from starlette.status import (
|
||||||
HTTP_500_INTERNAL_SERVER_ERROR,
|
HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
)
|
)
|
||||||
|
|
||||||
from haiku.rag.a2a import create_a2a_app
|
|
||||||
|
|
||||||
# OAuth2 Configuration
|
# OAuth2 Configuration
|
||||||
OAUTH2_TOKEN_URL = os.getenv(
|
OAUTH2_TOKEN_URL = os.getenv(
|
||||||
"OAUTH2_TOKEN_URL", "https://your-auth.example.com/oauth/token"
|
"OAUTH2_TOKEN_URL", "https://your-auth.example.com/oauth/token"
|
||||||
|
|
@ -24,12 +24,11 @@ import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
|
from haiku_rag_a2a.a2a import create_a2a_app
|
||||||
from starlette.exceptions import HTTPException
|
from starlette.exceptions import HTTPException
|
||||||
from starlette.responses import JSONResponse
|
from starlette.responses import JSONResponse
|
||||||
from starlette.status import HTTP_401_UNAUTHORIZED
|
from starlette.status import HTTP_401_UNAUTHORIZED
|
||||||
|
|
||||||
from haiku.rag.a2a import create_a2a_app
|
|
||||||
|
|
||||||
# Configuration
|
# Configuration
|
||||||
GITHUB_API_URL = "https://api.github.com"
|
GITHUB_API_URL = "https://api.github.com"
|
||||||
ALLOWED_TOKENS = (
|
ALLOWED_TOKENS = (
|
||||||
|
|
@ -5,6 +5,13 @@ import pytest
|
||||||
pytest.importorskip("fasta2a")
|
pytest.importorskip("fasta2a")
|
||||||
|
|
||||||
from fasta2a.schema import Message, TextPart # noqa: E402
|
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
|
from pydantic_ai.messages import ( # noqa: E402
|
||||||
ModelMessage,
|
ModelMessage,
|
||||||
ModelRequest,
|
ModelRequest,
|
||||||
|
|
@ -16,13 +23,6 @@ from pydantic_ai.messages import (
|
||||||
TextPart as AITextPart,
|
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
|
from haiku.rag.client import HaikuRAG
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -209,7 +209,7 @@ async def test_lru_memory_storage_access_order():
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_a2a_app_creation(temp_db_path):
|
async def test_a2a_app_creation(temp_db_path):
|
||||||
"""Test that A2A app can be created successfully."""
|
"""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
|
# Create a test database
|
||||||
async with HaikuRAG(temp_db_path) as client:
|
async with HaikuRAG(temp_db_path) as client:
|
||||||
|
|
@ -230,7 +230,7 @@ async def test_a2a_app_creation(temp_db_path):
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_a2a_app_has_skills(temp_db_path):
|
async def test_a2a_app_has_skills(temp_db_path):
|
||||||
"""Test that A2A app exposes skills describing its capabilities."""
|
"""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
|
# Create a test database
|
||||||
async with HaikuRAG(temp_db_path) as client:
|
async with HaikuRAG(temp_db_path) as client:
|
||||||
|
|
@ -291,6 +291,7 @@ def test_get_agent_skills():
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_build_artifacts_for_search():
|
async def test_build_artifacts_for_search():
|
||||||
"""Test that search operations produce structured search artifacts."""
|
"""Test that search operations produce structured search artifacts."""
|
||||||
|
from haiku_rag_a2a.a2a.worker import ConversationalWorker
|
||||||
from pydantic_ai.messages import (
|
from pydantic_ai.messages import (
|
||||||
ModelRequest,
|
ModelRequest,
|
||||||
ModelResponse,
|
ModelResponse,
|
||||||
|
|
@ -298,8 +299,6 @@ async def test_build_artifacts_for_search():
|
||||||
ToolReturnPart,
|
ToolReturnPart,
|
||||||
)
|
)
|
||||||
|
|
||||||
from haiku.rag.a2a.worker import ConversationalWorker
|
|
||||||
|
|
||||||
class MockResult:
|
class MockResult:
|
||||||
output = "Found 1 relevant results:\n\n1. *Score: 0.9* | **test**\nresult"
|
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
|
@pytest.mark.asyncio
|
||||||
async def test_build_artifacts_for_retrieve():
|
async def test_build_artifacts_for_retrieve():
|
||||||
"""Test that retrieve operations produce document artifacts."""
|
"""Test that retrieve operations produce document artifacts."""
|
||||||
|
from haiku_rag_a2a.a2a.worker import ConversationalWorker
|
||||||
from pydantic_ai.messages import (
|
from pydantic_ai.messages import (
|
||||||
ModelRequest,
|
ModelRequest,
|
||||||
ModelResponse,
|
ModelResponse,
|
||||||
|
|
@ -357,8 +357,6 @@ async def test_build_artifacts_for_retrieve():
|
||||||
ToolReturnPart,
|
ToolReturnPart,
|
||||||
)
|
)
|
||||||
|
|
||||||
from haiku.rag.a2a.worker import ConversationalWorker
|
|
||||||
|
|
||||||
class MockResult:
|
class MockResult:
|
||||||
output = "Document content"
|
output = "Document content"
|
||||||
|
|
||||||
|
|
@ -407,6 +405,7 @@ async def test_build_artifacts_for_retrieve():
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_build_artifacts_for_multiple_searches():
|
async def test_build_artifacts_for_multiple_searches():
|
||||||
"""Test that multiple searches each get their own artifact with correct results."""
|
"""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 (
|
from pydantic_ai.messages import (
|
||||||
ModelRequest,
|
ModelRequest,
|
||||||
ModelResponse,
|
ModelResponse,
|
||||||
|
|
@ -415,8 +414,6 @@ async def test_build_artifacts_for_multiple_searches():
|
||||||
)
|
)
|
||||||
from pydantic_ai.messages import TextPart as AITextPart
|
from pydantic_ai.messages import TextPart as AITextPart
|
||||||
|
|
||||||
from haiku.rag.a2a.worker import ConversationalWorker
|
|
||||||
|
|
||||||
class MockResult:
|
class MockResult:
|
||||||
output = "Answer based on multiple searches"
|
output = "Answer based on multiple searches"
|
||||||
|
|
||||||
|
|
@ -513,11 +510,10 @@ async def test_build_artifacts_for_multiple_searches():
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_qa_artifact_for_conversational_messages():
|
async def test_qa_artifact_for_conversational_messages():
|
||||||
"""Test that conversational Q&A messages always create qa_result artifacts."""
|
"""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 ModelResponse
|
||||||
from pydantic_ai.messages import TextPart as AITextPart
|
from pydantic_ai.messages import TextPart as AITextPart
|
||||||
|
|
||||||
from haiku.rag.a2a.worker import ConversationalWorker
|
|
||||||
|
|
||||||
class MockResult:
|
class MockResult:
|
||||||
output = "Hello! How can I help you?"
|
output = "Hello! How can I help you?"
|
||||||
|
|
||||||
|
|
@ -552,6 +548,7 @@ async def test_qa_artifact_for_conversational_messages():
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_build_artifacts_for_qa():
|
async def test_build_artifacts_for_qa():
|
||||||
"""Test that Q&A operations produce artifacts for each tool call."""
|
"""Test that Q&A operations produce artifacts for each tool call."""
|
||||||
|
from haiku_rag_a2a.a2a.worker import ConversationalWorker
|
||||||
from pydantic_ai.messages import (
|
from pydantic_ai.messages import (
|
||||||
ModelRequest,
|
ModelRequest,
|
||||||
ModelResponse,
|
ModelResponse,
|
||||||
|
|
@ -560,8 +557,6 @@ async def test_build_artifacts_for_qa():
|
||||||
)
|
)
|
||||||
from pydantic_ai.messages import TextPart as AITextPart
|
from pydantic_ai.messages import TextPart as AITextPart
|
||||||
|
|
||||||
from haiku.rag.a2a.worker import ConversationalWorker
|
|
||||||
|
|
||||||
class MockResult:
|
class MockResult:
|
||||||
output = "This is the answer"
|
output = "This is the answer"
|
||||||
|
|
||||||
4373
examples/a2a-server/uv.lock
Normal file
4373
examples/a2a-server/uv.lock
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,6 +1,6 @@
|
||||||
# haiku.rag Docker Compose Example
|
# 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
|
## Quick Start
|
||||||
|
|
||||||
|
|
@ -23,14 +23,10 @@ docker compose exec haiku-rag haiku-rag search "your query"
|
||||||
|
|
||||||
# Ask questions
|
# Ask questions
|
||||||
docker compose exec haiku-rag haiku-rag ask "What is haiku.rag?"
|
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
|
## Ports
|
||||||
|
|
||||||
- `8000` - A2A agent
|
|
||||||
- `8001` - MCP server
|
- `8001` - MCP server
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
@ -52,4 +48,3 @@ docker compose up -d
|
||||||
- [Configuration](https://ggozad.github.io/haiku.rag/configuration/)
|
- [Configuration](https://ggozad.github.io/haiku.rag/configuration/)
|
||||||
- [CLI Commands](https://ggozad.github.io/haiku.rag/cli/)
|
- [CLI Commands](https://ggozad.github.io/haiku.rag/cli/)
|
||||||
- [MCP Server](https://ggozad.github.io/haiku.rag/mcp/)
|
- [MCP Server](https://ggozad.github.io/haiku.rag/mcp/)
|
||||||
- [A2A Agent](https://ggozad.github.io/haiku.rag/a2a/)
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ services:
|
||||||
dockerfile: docker/Dockerfile
|
dockerfile: docker/Dockerfile
|
||||||
container_name: haiku-rag
|
container_name: haiku-rag
|
||||||
ports:
|
ports:
|
||||||
- "8000:8000" # A2A server
|
|
||||||
- "8001:8001" # MCP server
|
- "8001:8001" # MCP server
|
||||||
volumes:
|
volumes:
|
||||||
- ./data:/data # Persist database
|
- ./data:/data # Persist database
|
||||||
|
|
@ -19,9 +18,3 @@ services:
|
||||||
- CO_API_KEY=${CO_API_KEY}
|
- CO_API_KEY=${CO_API_KEY}
|
||||||
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 10s
|
|
||||||
retries: 3
|
|
||||||
start_period: 40s
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
Retrieval-Augmented Generation (RAG) library built on LanceDB - Core package with minimal dependencies.
|
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.
|
**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
|
- `bedrock` - AWS Bedrock
|
||||||
- `vertexai` - Google Vertex AI
|
- `vertexai` - Google Vertex AI
|
||||||
|
|
||||||
**Agent Protocol:**
|
|
||||||
- `a2a` - Agent-to-Agent protocol
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Common combinations
|
# Common combinations
|
||||||
|
|
@ -64,7 +62,6 @@ See the main [`haiku.rag`](https://github.com/ggozad/haiku.rag) repository for:
|
||||||
- CLI examples
|
- CLI examples
|
||||||
- Python API usage
|
- Python API usage
|
||||||
- MCP server setup
|
- MCP server setup
|
||||||
- A2A agent configuration
|
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -455,9 +455,6 @@ class HaikuRAGApp:
|
||||||
enable_mcp: bool = True,
|
enable_mcp: bool = True,
|
||||||
mcp_transport: str | None = None,
|
mcp_transport: str | None = None,
|
||||||
mcp_port: int = 8001,
|
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."""
|
"""Start the server with selected services."""
|
||||||
async with HaikuRAG(self.db_path, config=self.config) as client:
|
async with HaikuRAG(self.db_path, config=self.config) as client:
|
||||||
|
|
@ -485,33 +482,6 @@ class HaikuRAGApp:
|
||||||
mcp_task = asyncio.create_task(run_mcp())
|
mcp_task = asyncio.create_task(run_mcp())
|
||||||
tasks.append(mcp_task)
|
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:
|
if not tasks:
|
||||||
logger.warning("No services enabled")
|
logger.warning("No services enabled")
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -401,7 +401,7 @@ def download_models_cmd():
|
||||||
|
|
||||||
@cli.command(
|
@cli.command(
|
||||||
"serve",
|
"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(
|
def serve(
|
||||||
db: Path | None = typer.Option(
|
db: Path | None = typer.Option(
|
||||||
|
|
@ -429,27 +429,12 @@ def serve(
|
||||||
"--mcp-port",
|
"--mcp-port",
|
||||||
help="Port to bind MCP server to (ignored with --stdio)",
|
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:
|
) -> None:
|
||||||
"""Start the server with selected services."""
|
"""Start the server with selected services."""
|
||||||
# Require at least one service flag
|
# Require at least one service flag
|
||||||
if not (monitor or mcp or a2a):
|
if not (monitor or mcp):
|
||||||
typer.echo(
|
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)
|
raise typer.Exit(1)
|
||||||
|
|
||||||
|
|
@ -467,34 +452,9 @@ def serve(
|
||||||
enable_mcp=mcp,
|
enable_mcp=mcp,
|
||||||
mcp_transport=transport,
|
mcp_transport=transport,
|
||||||
mcp_port=mcp_port,
|
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__":
|
if __name__ == "__main__":
|
||||||
cli()
|
cli()
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ from haiku.rag.config.loader import (
|
||||||
load_yaml_config,
|
load_yaml_config,
|
||||||
)
|
)
|
||||||
from haiku.rag.config.models import (
|
from haiku.rag.config.models import (
|
||||||
A2AConfig,
|
|
||||||
AppConfig,
|
AppConfig,
|
||||||
EmbeddingsConfig,
|
EmbeddingsConfig,
|
||||||
LanceDBConfig,
|
LanceDBConfig,
|
||||||
|
|
@ -35,7 +34,6 @@ __all__ = [
|
||||||
"OllamaConfig",
|
"OllamaConfig",
|
||||||
"VLLMConfig",
|
"VLLMConfig",
|
||||||
"ProvidersConfig",
|
"ProvidersConfig",
|
||||||
"A2AConfig",
|
|
||||||
"find_config_file",
|
"find_config_file",
|
||||||
"load_yaml_config",
|
"load_yaml_config",
|
||||||
"generate_default_config",
|
"generate_default_config",
|
||||||
|
|
|
||||||
|
|
@ -84,5 +84,4 @@ def generate_default_config() -> dict:
|
||||||
"research_base_url": "",
|
"research_base_url": "",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"a2a": {"max_contexts": 1000},
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -76,10 +76,6 @@ class ProvidersConfig(BaseModel):
|
||||||
vllm: VLLMConfig = Field(default_factory=VLLMConfig)
|
vllm: VLLMConfig = Field(default_factory=VLLMConfig)
|
||||||
|
|
||||||
|
|
||||||
class A2AConfig(BaseModel):
|
|
||||||
max_contexts: int = 1000
|
|
||||||
|
|
||||||
|
|
||||||
class AppConfig(BaseModel):
|
class AppConfig(BaseModel):
|
||||||
environment: str = "production"
|
environment: str = "production"
|
||||||
storage: StorageConfig = Field(default_factory=StorageConfig)
|
storage: StorageConfig = Field(default_factory=StorageConfig)
|
||||||
|
|
@ -91,4 +87,3 @@ class AppConfig(BaseModel):
|
||||||
research: ResearchConfig = Field(default_factory=ResearchConfig)
|
research: ResearchConfig = Field(default_factory=ResearchConfig)
|
||||||
processing: ProcessingConfig = Field(default_factory=ProcessingConfig)
|
processing: ProcessingConfig = Field(default_factory=ProcessingConfig)
|
||||||
providers: ProvidersConfig = Field(default_factory=ProvidersConfig)
|
providers: ProvidersConfig = Field(default_factory=ProvidersConfig)
|
||||||
a2a: A2AConfig = Field(default_factory=A2AConfig)
|
|
||||||
|
|
|
||||||
|
|
@ -45,8 +45,6 @@ voyageai = ["voyageai>=0.3.5"]
|
||||||
mxbai = ["mxbai-rerank>=0.1.6"]
|
mxbai = ["mxbai-rerank>=0.1.6"]
|
||||||
cohere = ["cohere>=5.0.0"]
|
cohere = ["cohere>=5.0.0"]
|
||||||
zeroentropy = ["zeroentropy>=0.1.0a6"]
|
zeroentropy = ["zeroentropy>=0.1.0a6"]
|
||||||
# Agent protocols
|
|
||||||
a2a = ["fasta2a>=0.1.0", "pydantic-ai-slim[a2a]"]
|
|
||||||
# Model providers (delegated to pydantic-ai-slim)
|
# Model providers (delegated to pydantic-ai-slim)
|
||||||
anthropic = ["pydantic-ai-slim[anthropic]"]
|
anthropic = ["pydantic-ai-slim[anthropic]"]
|
||||||
groq = ["pydantic-ai-slim[groq]"]
|
groq = ["pydantic-ai-slim[groq]"]
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,6 @@ nav:
|
||||||
- Agents: agents.md
|
- Agents: agents.md
|
||||||
- Server: server.md
|
- Server: server.md
|
||||||
- MCP: mcp.md
|
- MCP: mcp.md
|
||||||
- A2A: a2a.md
|
|
||||||
- Benchmarks: benchmarks.md
|
- Benchmarks: benchmarks.md
|
||||||
markdown_extensions:
|
markdown_extensions:
|
||||||
- admonition
|
- admonition
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ classifiers = [
|
||||||
"Typing :: Typed",
|
"Typing :: Typed",
|
||||||
]
|
]
|
||||||
|
|
||||||
dependencies = ["haiku.rag-slim[docling,voyageai,mxbai,a2a,cohere,zeroentropy]"]
|
dependencies = ["haiku.rag-slim[docling,voyageai,mxbai,cohere,zeroentropy]"]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
haiku-rag = "haiku.rag.cli:cli"
|
haiku-rag = "haiku.rag.cli:cli"
|
||||||
|
|
@ -100,6 +100,8 @@ pythonVersion = "3.12"
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
asyncio_default_fixture_loop_scope = "session"
|
asyncio_default_fixture_loop_scope = "session"
|
||||||
asyncio_mode = "auto"
|
asyncio_mode = "auto"
|
||||||
|
testpaths = ["tests"]
|
||||||
|
norecursedirs = ["examples", "docs", "evaluations", ".git", ".venv"]
|
||||||
|
|
||||||
# pyproject.toml
|
# pyproject.toml
|
||||||
filterwarnings = ["error", "ignore::UserWarning", "ignore::DeprecationWarning"]
|
filterwarnings = ["error", "ignore::UserWarning", "ignore::DeprecationWarning"]
|
||||||
|
|
|
||||||
|
|
@ -211,7 +211,6 @@ async def test_serve_mcp_only(app: HaikuRAGApp, monkeypatch, transport):
|
||||||
enable_monitor=False,
|
enable_monitor=False,
|
||||||
enable_mcp=True,
|
enable_mcp=True,
|
||||||
mcp_transport=transport,
|
mcp_transport=transport,
|
||||||
enable_a2a=False,
|
|
||||||
)
|
)
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
pass
|
pass
|
||||||
|
|
@ -245,50 +244,16 @@ async def test_serve_monitor_only(app: HaikuRAGApp, monkeypatch):
|
||||||
|
|
||||||
with patch("haiku.rag.app.HaikuRAG", return_value=mock_client):
|
with patch("haiku.rag.app.HaikuRAG", return_value=mock_client):
|
||||||
try:
|
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:
|
except asyncio.CancelledError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
assert len(created_tasks) == 1
|
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
|
@pytest.mark.asyncio
|
||||||
async def test_serve_all_services(app: HaikuRAGApp, monkeypatch):
|
async def test_serve_all_services(app: HaikuRAGApp, monkeypatch):
|
||||||
"""Test the serve method with all services enabled."""
|
"""Test the serve method with all services enabled."""
|
||||||
pytest.importorskip("fasta2a")
|
|
||||||
created_tasks = []
|
created_tasks = []
|
||||||
original_create_task = asyncio.create_task
|
original_create_task = asyncio.create_task
|
||||||
|
|
||||||
|
|
@ -300,7 +265,6 @@ async def test_serve_all_services(app: HaikuRAGApp, monkeypatch):
|
||||||
|
|
||||||
mock_server = AsyncMock()
|
mock_server = AsyncMock()
|
||||||
mock_watcher = AsyncMock()
|
mock_watcher = AsyncMock()
|
||||||
mock_a2a_app = MagicMock()
|
|
||||||
|
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
"haiku.rag.app.create_mcp_server", MagicMock(return_value=mock_server)
|
"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
|
mock_client.__aenter__.return_value = mock_client
|
||||||
|
|
||||||
with patch("haiku.rag.app.HaikuRAG", 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:
|
||||||
try:
|
await app.serve(enable_monitor=True, enable_mcp=True)
|
||||||
await app.serve(enable_monitor=True, enable_mcp=True, enable_a2a=True)
|
except asyncio.CancelledError:
|
||||||
except asyncio.CancelledError:
|
pass
|
||||||
pass
|
|
||||||
|
|
||||||
assert len(created_tasks) == 3
|
assert len(created_tasks) == 2
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|
|
||||||
|
|
@ -196,7 +196,6 @@ def test_serve_mcp_only():
|
||||||
_, kwargs = mock_app_instance.serve.call_args
|
_, kwargs = mock_app_instance.serve.call_args
|
||||||
assert kwargs["enable_monitor"] is False
|
assert kwargs["enable_monitor"] is False
|
||||||
assert kwargs["enable_mcp"] is True
|
assert kwargs["enable_mcp"] is True
|
||||||
assert kwargs["enable_a2a"] is False
|
|
||||||
assert kwargs["mcp_transport"] is None
|
assert kwargs["mcp_transport"] is None
|
||||||
assert kwargs["mcp_port"] == 8001
|
assert kwargs["mcp_port"] == 8001
|
||||||
|
|
||||||
|
|
@ -230,26 +229,6 @@ def test_serve_monitor_only():
|
||||||
_, kwargs = mock_app_instance.serve.call_args
|
_, kwargs = mock_app_instance.serve.call_args
|
||||||
assert kwargs["enable_monitor"] is True
|
assert kwargs["enable_monitor"] is True
|
||||||
assert kwargs["enable_mcp"] is False
|
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():
|
def test_serve_all_services():
|
||||||
|
|
@ -259,32 +238,28 @@ def test_serve_all_services():
|
||||||
mock_app_instance.serve = AsyncMock()
|
mock_app_instance.serve = AsyncMock()
|
||||||
mock_app.return_value = mock_app_instance
|
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
|
assert result.exit_code == 0
|
||||||
mock_app_instance.serve.assert_called_once()
|
mock_app_instance.serve.assert_called_once()
|
||||||
_, kwargs = mock_app_instance.serve.call_args
|
_, kwargs = mock_app_instance.serve.call_args
|
||||||
assert kwargs["enable_monitor"] is True
|
assert kwargs["enable_monitor"] is True
|
||||||
assert kwargs["enable_mcp"] is True
|
assert kwargs["enable_mcp"] is True
|
||||||
assert kwargs["enable_a2a"] is True
|
|
||||||
|
|
||||||
|
|
||||||
def test_serve_custom_ports():
|
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:
|
with patch("haiku.rag.cli.HaikuRAGApp") as mock_app:
|
||||||
mock_app_instance = MagicMock()
|
mock_app_instance = MagicMock()
|
||||||
mock_app_instance.serve = AsyncMock()
|
mock_app_instance.serve = AsyncMock()
|
||||||
mock_app.return_value = mock_app_instance
|
mock_app.return_value = mock_app_instance
|
||||||
|
|
||||||
result = runner.invoke(
|
result = runner.invoke(cli, ["serve", "--mcp", "--mcp-port", "9000"])
|
||||||
cli, ["serve", "--mcp", "--mcp-port", "9000", "--a2a", "--a2a-port", "9001"]
|
|
||||||
)
|
|
||||||
|
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
mock_app_instance.serve.assert_called_once()
|
mock_app_instance.serve.assert_called_once()
|
||||||
_, kwargs = mock_app_instance.serve.call_args
|
_, kwargs = mock_app_instance.serve.call_args
|
||||||
assert kwargs["mcp_port"] == 9000
|
assert kwargs["mcp_port"] == 9000
|
||||||
assert kwargs["a2a_port"] == 9001
|
|
||||||
|
|
||||||
|
|
||||||
def test_serve_stdio_without_mcp():
|
def test_serve_stdio_without_mcp():
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue