Hayhooks

repository·main·Indexed 40 days ago

https://github.com/deepset-ai/hayhooks

A deployment framework for Haystack Pipelines and Agents that converts Haystack logic into production-ready REST APIs, OpenAI-compatible chat backends, MCP tools, and interactive UIs. It includes a Tracing Dashboard for visualizing live activity and supports integrations with Chainlit and Codex.

Tokens
75.8K
Snippets
261
Records
333
Agent score
95%

What's inside Hayhooks

  1. Overview of Hayhooks Pipeline Wrapper Examples

    main

    Hayhooks provides several pipeline wrapper patterns to handle different AI workflows. Key patterns include:

    • Streaming Support: Examples like multi_llm_streaming and async_question_answer demonstrate automatic multi-component streaming and OpenAI-compatible chat interfaces.
    • Hybrid Sync/Async: The async_hybrid_streaming example shows how to use allow_sync_streaming_callbacks=True to bridge legacy synchronous components into an asynchronous pipeline.
    • Agentic Workflows: Examples like open_webui_agent_events and open_webui_agent_on_tool_calls demonstrate how to integrate agents with Open WebUI, providing real-time status updates and tool-call lifecycle feedback.
    • File & Binary Handling: image_generation shows how to return FileResponse (e.g., images) directly, while chat_completion_with_file_upload demonstrates using the /v1/files endpoint with OpenAI-compatible multi-part content formats.
    • Multi-Agent Systems: a2a_multi_agent demonstrates Agent-to-Agent (A2A) communication where agents delegate tasks to each other using an A2A client tool and MCP (Model Context Protocol).
    • RAG Implementations: rag_indexing_query provides a complete pattern for document indexing and querying using Elasticsearch.
  2. Input and Output handling in Hayhooks

    main

    Hayhooks automates several data handling tasks for deployed pipelines:

    • Validation: Uses Pydantic models to validate incoming requests.
    • Serialization: Handles JSON serialization for all responses.
    • File Uploads: Supports multipart/form-data requests for handling file uploads.
    • Type Conversion: Automatically converts between JSON and Python types.
  3. Choose a pipeline deployment method

    main

    Hayhooks offers two primary ways to deploy Haystack pipelines and agents depending on your requirements for flexibility and complexity:

    1. PipelineWrapper Deployment (Recommended): Use this for complex logic. It involves creating a class that encapsulates your pipeline. This method supports:

      • Custom initialization and execution logic.
      • OpenAI-compatible chat endpoints.
      • Streaming capabilities.
      • File upload handling.
    2. YAML Pipeline Deployment: Use this for straightforward pipelines. You deploy directly from YAML definitions. This method provides:

      • Automatic request/response schema generation.
      • No wrapper code required.
      • Limitations: Does NOT support OpenAI-compatible chat endpoints or streaming. The YAML must include inputs and outputs sections with properly defined pipeline components.
  4. Limitations of YAML-deployed pipelines

    main

    YAML-deployed pipelines are simplified and do not support all Hayhooks features. If you require the following, you must use a PipelineWrapper instead:

    • No OpenAI Compatibility: Does not support OpenAI-compatible chat endpoints.
    • No Streaming: Streaming responses are not supported.
    • No File Uploads: File upload handling is unavailable.
    • Async Only: Pipelines are executed using Pipeline.run_async().
  5. Returning binary files (images, PDFs, etc.) from a run_api endpoint

    main

    By default, Hayhooks wraps responses in a Pydantic JSON object. However, if your run_api method returns a FastAPI Response object (such as FileResponse, StreamingResponse, or a plain Response), Hayhooks will bypass the JSON wrapper and pass the object directly to the client. This allows you to serve binary content like images, PDFs, or audio files directly from your pipeline.

    # If your run_api returns a FastAPI Response, Hayhooks passes it directly
    from fastapi.responses import FileResponse
    
    # Inside your pipeline wrapper's run_api method:
    return FileResponse("path/to/image.png")
  6. How custom elements work in Chainlit

    main

    Hayhooks allows agents to trigger custom UI components in the Chainlit interface by emitting custom_element events.

    1. Emission: The pipeline wrapper calls create_custom_element_event("ComponentName", props) to send an event over the SSE stream.
    2. Rendering: The Chainlit app receives the event and renders a cl.CustomElement(name="ComponentName", props=...).
    3. Resolution: Hayhooks resolves these components by copying .jsx files from the directory specified by --chainlit-custom-elements-dir (or the HAYHOOKS_CHAINLIT_CUSTOM_ELEMENTS_DIR environment variable) into the Chainlit public/elements/ directory at startup.

    Note: custom_element events are ignored by Open WebUI, but status and notification events will still render normally.

  7. Inject custom events using external_event_queue

    main

    Both streaming_generator and async_streaming_generator accept an external_event_queue parameter. This allows you to inject custom events (such as dict, OpenWebUIEvent, str, or StreamingChunk) into the stream alongside standard pipeline chunks.

    Typical Use Case: Passing the queue to pipeline components to implement Human-in-the-Loop (HITL) patterns, where a component can push an approval request event to the queue while waiting for user input.

  8. How Hayhooks automatically creates MCP Tools from pipelines

    main

    Hayhooks automatically exposes deployed pipelines as MCP tools. The tool's metadata is derived from your code:

    • Name: Uses the PipelineWrapper name.
    • Description: Parsed from the run_api method docstring. The first line is used as the tool description. Subsequent lines/sections are used for parameter descriptions.
    • Input Schema: Generated from the run_api method arguments using Pydantic.

    Example Implementation:

    from pathlib import Path
    from haystack import Pipeline
    from hayhooks import BasePipelineWrapper
    
    class PipelineWrapper(BasePipelineWrapper):
        def setup(self) -> None:
            pipeline_yaml = (Path(__file__).parent / "chat_with_website.yml").read_text()
            self.pipeline = Pipeline.loads(pipeline_yaml)
    
        def run_api(self, urls: list[str], question: str) -> str:
            """
            Ask a question about one or more websites using a Haystack pipeline.
            """
            result = self.pipeline.run({"fetcher": {"urls": urls}, "prompt": {"query": question}})
            return result["llm"]["replies"][0]

    Note on YAML Pipelines: Pipelines deployed via hayhooks pipeline deploy-yaml are also automatically exposed, with the input schema derived from the YAML inputs section.

  9. How the Chainlit integration works

    main

    The Chainlit UI is mounted as a FastAPI sub-application. It communicates with your pipelines through Hayhooks' OpenAI-compatible endpoints (/v1/chat/completions).

    For the integration to work:

    1. Your pipelines must implement run_chat_completion or run_chat_completion_async.
    2. The UI automatically discovers available pipelines via the /v1/models endpoint.
    3. Streaming is supported out-of-the-box if your pipeline returns a Generator or AsyncGenerator (ideally using streaming_generator or async_streaming_generator).
  10. Optimize I/O-bound pipelines

    main

    I/O-bound pipelines (waiting on LLM APIs, databases, or HTTP requests) should use asynchronous methods to maximize throughput. Implement run_api_async() or run_chat_completion_async() in your BasePipelineWrapper. Scaling horizontally (adding more replicas/containers) is more effective than adding more workers to a single instance for I/O-bound workloads.

    from hayhooks import BasePipelineWrapper, Pipeline
    
    class PipelineWrapper(BasePipelineWrapper):
        def setup(self) -> None:
            self.pipeline = Pipeline()
            # ... build pipeline ...
    
        async def run_api_async(self, query: str) -> str:
            result = await self.pipeline.run_async({"prompt": {"query": query}})
            return result["llm"]["replies"][0]