tstadel 2c56305ed3
Fix serialization of numpy arrays and pandas dataframes in REST API (#2838)
* correct serialization of numpy arrays and pandas dataframes

* Update Documentation & Code Style

* set additional json_encoders globally

* Update Documentation & Code Style

* add tests for non primitive return types

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2022-08-02 09:49:32 +02:00

121 lines
3.7 KiB
Python

from typing import Dict, Any
import collections
import logging
import time
import json
from pydantic import BaseConfig
from fastapi import FastAPI, APIRouter
import haystack
from haystack import Pipeline
from haystack.telemetry import send_event_if_public_demo
from rest_api.utils import get_app, get_pipelines
from rest_api.config import LOG_LEVEL
from rest_api.schema import QueryRequest, QueryResponse
logging.getLogger("haystack").setLevel(LOG_LEVEL)
logger = logging.getLogger("haystack")
BaseConfig.arbitrary_types_allowed = True
router = APIRouter()
app: FastAPI = get_app()
query_pipeline: Pipeline = get_pipelines().get("query_pipeline", None)
concurrency_limiter = get_pipelines().get("concurrency_limiter", None)
@router.get("/initialized")
def check_status():
"""
This endpoint can be used during startup to understand if the
server is ready to take any requests, or is still loading.
The recommended approach is to call this endpoint with a short timeout,
like 500ms, and in case of no reply, consider the server busy.
"""
return True
@router.get("/hs_version")
def haystack_version():
"""
Get the running Haystack version.
"""
return {"hs_version": haystack.__version__}
@router.post("/query", response_model=QueryResponse, response_model_exclude_none=True)
def query(request: QueryRequest):
"""
This endpoint receives the question as a string and allows the requester to set
additional parameters that will be passed on to the Haystack pipeline.
"""
with concurrency_limiter.run():
result = _process_request(query_pipeline, request)
return result
@send_event_if_public_demo
def _process_request(pipeline, request) -> Dict[str, Any]:
start_time = time.time()
params = request.params or {}
# format global, top-level filters (e.g. "params": {"filters": {"name": ["some"]}})
if "filters" in params.keys():
params["filters"] = _format_filters(params["filters"])
# format targeted node filters (e.g. "params": {"Retriever": {"filters": {"value"}}})
for key in params.keys():
if isinstance(params[key], collections.Mapping) and "filters" in params[key].keys():
params[key]["filters"] = _format_filters(params[key]["filters"])
result = pipeline.run(query=request.query, params=params, debug=request.debug)
# Ensure answers and documents exist, even if they're empty lists
if not "documents" in result:
result["documents"] = []
if not "answers" in result:
result["answers"] = []
logger.info(
json.dumps({"request": request, "response": result, "time": f"{(time.time() - start_time):.2f}"}, default=str)
)
return result
def _format_filters(filters):
"""
Adjust filters to compliant format:
Put filter values into a list and remove filters with null value.
"""
new_filters = {}
if filters is None:
logger.warning(
f"Request with deprecated filter format ('\"filters\": null'). "
f"Remove empty filters from params to be compliant with future versions"
)
else:
for key, values in filters.items():
if values is None:
logger.warning(
f"Request with deprecated filter format ('{key}: null'). "
f"Remove null values from filters to be compliant with future versions"
)
continue
if not isinstance(values, list):
logger.warning(
f"Request with deprecated filter format ('{key}': {values}). "
f"Change to '{key}':[{values}]' to be compliant with future versions"
)
values = [values]
new_filters[key] = values
return new_filters