mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-05-06 02:07:49 +08:00
Refa: Searches /search API to RESTFul (#13770)
### What problem does this PR solve? Searches /search API to RESTFul ### Type of change - [x] Documentation Update - [x] Refactoring Co-authored-by: Jin Hai <haijin.chn@gmail.com> Co-authored-by: Yingfeng <yingfeng.zhang@gmail.com>
This commit is contained in:
@ -24,10 +24,10 @@ from api.db.services.search_service import SearchService
|
|||||||
from api.db.services.user_service import TenantService, UserTenantService
|
from api.db.services.user_service import TenantService, UserTenantService
|
||||||
from common.misc_utils import get_uuid
|
from common.misc_utils import get_uuid
|
||||||
from common.constants import RetCode, StatusEnum
|
from common.constants import RetCode, StatusEnum
|
||||||
from api.utils.api_utils import get_data_error_result, get_json_result, not_allowed_parameters, get_request_json, server_error_response, validate_request
|
from api.utils.api_utils import get_data_error_result, get_json_result, get_request_json, server_error_response, validate_request
|
||||||
|
|
||||||
|
|
||||||
@manager.route("/create", methods=["post"]) # noqa: F821
|
@manager.route("/searches", methods=["POST"]) # noqa: F821
|
||||||
@login_required
|
@login_required
|
||||||
@validate_request("name")
|
@validate_request("name")
|
||||||
async def create():
|
async def create():
|
||||||
@ -61,67 +61,34 @@ async def create():
|
|||||||
return server_error_response(e)
|
return server_error_response(e)
|
||||||
|
|
||||||
|
|
||||||
@manager.route("/update", methods=["post"]) # noqa: F821
|
@manager.route("/searches", methods=["GET"]) # noqa: F821
|
||||||
@login_required
|
@login_required
|
||||||
@validate_request("search_id", "name", "search_config", "tenant_id")
|
def list_searches():
|
||||||
@not_allowed_parameters("id", "created_by", "create_time", "update_time", "create_date", "update_date", "created_by")
|
keywords = request.args.get("keywords", "")
|
||||||
async def update():
|
page_number = int(request.args.get("page", 0))
|
||||||
req = await get_request_json()
|
items_per_page = int(request.args.get("page_size", 0))
|
||||||
if not isinstance(req["name"], str):
|
orderby = request.args.get("orderby", "create_time")
|
||||||
return get_data_error_result(message="Search name must be string.")
|
desc = request.args.get("desc", "true").lower() != "false"
|
||||||
if req["name"].strip() == "":
|
owner_ids = request.args.getlist("owner_ids")
|
||||||
return get_data_error_result(message="Search name can't be empty.")
|
|
||||||
if len(req["name"].encode("utf-8")) > DATASET_NAME_LIMIT:
|
|
||||||
return get_data_error_result(message=f"Search name length is {len(req['name'])} which is large than {DATASET_NAME_LIMIT}")
|
|
||||||
req["name"] = req["name"].strip()
|
|
||||||
tenant_id = req["tenant_id"]
|
|
||||||
e, _ = TenantService.get_by_id(tenant_id)
|
|
||||||
if not e:
|
|
||||||
return get_data_error_result(message="Authorized identity.")
|
|
||||||
|
|
||||||
search_id = req["search_id"]
|
|
||||||
if not SearchService.accessible4deletion(search_id, current_user.id):
|
|
||||||
return get_json_result(data=False, message="No authorization.", code=RetCode.AUTHENTICATION_ERROR)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
search_app = SearchService.query(tenant_id=tenant_id, id=search_id)[0]
|
if not owner_ids:
|
||||||
if not search_app:
|
tenants = []
|
||||||
return get_json_result(data=False, message=f"Cannot find search {search_id}", code=RetCode.DATA_ERROR)
|
search_apps, total = SearchService.get_by_tenant_ids(tenants, current_user.id, page_number, items_per_page, orderby, desc, keywords)
|
||||||
|
else:
|
||||||
if req["name"].lower() != search_app.name.lower() and len(SearchService.query(name=req["name"], tenant_id=tenant_id, status=StatusEnum.VALID.value)) >= 1:
|
search_apps, total = SearchService.get_by_tenant_ids(owner_ids, current_user.id, 0, 0, orderby, desc, keywords)
|
||||||
return get_data_error_result(message="Duplicated search name.")
|
search_apps = [s for s in search_apps if s["tenant_id"] in owner_ids]
|
||||||
|
total = len(search_apps)
|
||||||
if "search_config" in req:
|
if page_number and items_per_page:
|
||||||
current_config = search_app.search_config or {}
|
search_apps = search_apps[(page_number - 1) * items_per_page: page_number * items_per_page]
|
||||||
new_config = req["search_config"]
|
return get_json_result(data={"search_apps": search_apps, "total": total})
|
||||||
|
|
||||||
if not isinstance(new_config, dict):
|
|
||||||
return get_data_error_result(message="search_config must be a JSON object")
|
|
||||||
|
|
||||||
updated_config = {**current_config, **new_config}
|
|
||||||
req["search_config"] = updated_config
|
|
||||||
|
|
||||||
req.pop("search_id", None)
|
|
||||||
req.pop("tenant_id", None)
|
|
||||||
|
|
||||||
updated = SearchService.update_by_id(search_id, req)
|
|
||||||
if not updated:
|
|
||||||
return get_data_error_result(message="Failed to update search")
|
|
||||||
|
|
||||||
e, updated_search = SearchService.get_by_id(search_id)
|
|
||||||
if not e:
|
|
||||||
return get_data_error_result(message="Failed to fetch updated search")
|
|
||||||
|
|
||||||
return get_json_result(data=updated_search.to_dict())
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return server_error_response(e)
|
return server_error_response(e)
|
||||||
|
|
||||||
|
|
||||||
@manager.route("/detail", methods=["GET"]) # noqa: F821
|
@manager.route("/searches/<search_id>", methods=["GET"]) # noqa: F821
|
||||||
@login_required
|
@login_required
|
||||||
def detail():
|
def detail(search_id):
|
||||||
search_id = request.args["search_id"]
|
|
||||||
try:
|
try:
|
||||||
tenants = UserTenantService.query(user_id=current_user.id)
|
tenants = UserTenantService.query(user_id=current_user.id)
|
||||||
for tenant in tenants:
|
for tenant in tenants:
|
||||||
@ -138,44 +105,60 @@ def detail():
|
|||||||
return server_error_response(e)
|
return server_error_response(e)
|
||||||
|
|
||||||
|
|
||||||
@manager.route("/list", methods=["POST"]) # noqa: F821
|
@manager.route("/searches/<search_id>", methods=["PUT"]) # noqa: F821
|
||||||
@login_required
|
@login_required
|
||||||
async def list_search_app():
|
@validate_request("name", "search_config")
|
||||||
keywords = request.args.get("keywords", "")
|
async def update(search_id):
|
||||||
page_number = int(request.args.get("page", 0))
|
|
||||||
items_per_page = int(request.args.get("page_size", 0))
|
|
||||||
orderby = request.args.get("orderby", "create_time")
|
|
||||||
if request.args.get("desc", "true").lower() == "false":
|
|
||||||
desc = False
|
|
||||||
else:
|
|
||||||
desc = True
|
|
||||||
|
|
||||||
req = await get_request_json()
|
req = await get_request_json()
|
||||||
owner_ids = req.get("owner_ids", [])
|
if not isinstance(req["name"], str):
|
||||||
|
return get_data_error_result(message="Search name must be string.")
|
||||||
|
if req["name"].strip() == "":
|
||||||
|
return get_data_error_result(message="Search name can't be empty.")
|
||||||
|
if len(req["name"].encode("utf-8")) > DATASET_NAME_LIMIT:
|
||||||
|
return get_data_error_result(message=f"Search name length is {len(req['name'])} which is large than {DATASET_NAME_LIMIT}")
|
||||||
|
req["name"] = req["name"].strip()
|
||||||
|
|
||||||
|
e, _ = TenantService.get_by_id(current_user.id)
|
||||||
|
if not e:
|
||||||
|
return get_data_error_result(message="Authorized identity.")
|
||||||
|
|
||||||
|
if not SearchService.accessible4deletion(search_id, current_user.id):
|
||||||
|
return get_json_result(data=False, message="No authorization.", code=RetCode.AUTHENTICATION_ERROR)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if not owner_ids:
|
search_app = SearchService.query(tenant_id=current_user.id, id=search_id)[0]
|
||||||
# tenants = TenantService.get_joined_tenants_by_user_id(current_user.id)
|
if not search_app:
|
||||||
# tenants = [m["tenant_id"] for m in tenants]
|
return get_json_result(data=False, message=f"Cannot find search {search_id}", code=RetCode.DATA_ERROR)
|
||||||
tenants = []
|
|
||||||
search_apps, total = SearchService.get_by_tenant_ids(tenants, current_user.id, page_number, items_per_page, orderby, desc, keywords)
|
if req["name"].lower() != search_app.name.lower() and len(SearchService.query(name=req["name"], tenant_id=current_user.id, status=StatusEnum.VALID.value)) >= 1:
|
||||||
else:
|
return get_data_error_result(message="Duplicated search name.")
|
||||||
tenants = owner_ids
|
|
||||||
search_apps, total = SearchService.get_by_tenant_ids(tenants, current_user.id, 0, 0, orderby, desc, keywords)
|
current_config = search_app.search_config or {}
|
||||||
search_apps = [search_app for search_app in search_apps if search_app["tenant_id"] in tenants]
|
new_config = req["search_config"]
|
||||||
total = len(search_apps)
|
if not isinstance(new_config, dict):
|
||||||
if page_number and items_per_page:
|
return get_data_error_result(message="search_config must be a JSON object")
|
||||||
search_apps = search_apps[(page_number - 1) * items_per_page : page_number * items_per_page]
|
req["search_config"] = {**current_config, **new_config}
|
||||||
return get_json_result(data={"search_apps": search_apps, "total": total})
|
|
||||||
|
for field in ("search_id", "tenant_id", "created_by", "update_time", "id"):
|
||||||
|
req.pop(field, None)
|
||||||
|
|
||||||
|
updated = SearchService.update_by_id(search_id, req)
|
||||||
|
if not updated:
|
||||||
|
return get_data_error_result(message="Failed to update search")
|
||||||
|
|
||||||
|
e, updated_search = SearchService.get_by_id(search_id)
|
||||||
|
if not e:
|
||||||
|
return get_data_error_result(message="Failed to fetch updated search")
|
||||||
|
|
||||||
|
return get_json_result(data=updated_search.to_dict())
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return server_error_response(e)
|
return server_error_response(e)
|
||||||
|
|
||||||
|
|
||||||
@manager.route("/rm", methods=["post"]) # noqa: F821
|
@manager.route("/searches/<search_id>", methods=["DELETE"]) # noqa: F821
|
||||||
@login_required
|
@login_required
|
||||||
@validate_request("search_id")
|
def delete_search(search_id):
|
||||||
async def rm():
|
|
||||||
req = await get_request_json()
|
|
||||||
search_id = req["search_id"]
|
|
||||||
if not SearchService.accessible4deletion(search_id, current_user.id):
|
if not SearchService.accessible4deletion(search_id, current_user.id):
|
||||||
return get_json_result(data=False, message="No authorization.", code=RetCode.AUTHENTICATION_ERROR)
|
return get_json_result(data=False, message="No authorization.", code=RetCode.AUTHENTICATION_ERROR)
|
||||||
|
|
||||||
@ -7034,3 +7034,305 @@ or
|
|||||||
"message": "Can't find this dataset!"
|
"message": "Can't find this dataset!"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## SEARCH APP MANAGEMENT
|
||||||
|
|
||||||
|
### Create search app
|
||||||
|
|
||||||
|
**POST** `/api/v1/searches`
|
||||||
|
|
||||||
|
Creates a search app.
|
||||||
|
|
||||||
|
#### Request
|
||||||
|
|
||||||
|
- Method: POST
|
||||||
|
- URL: `/api/v1/searches`
|
||||||
|
- Headers:
|
||||||
|
- `'Content-Type: application/json'`
|
||||||
|
- `'Authorization: Bearer <YOUR_API_KEY>'`
|
||||||
|
- Body:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "my_search_app",
|
||||||
|
"description": "optional description"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Request example
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl --request POST \
|
||||||
|
--url 'http://{address}/api/v1/searches' \
|
||||||
|
--header 'Authorization: Bearer <YOUR_API_KEY>' \
|
||||||
|
--header 'Content-Type: application/json' \
|
||||||
|
--data '{
|
||||||
|
"name": "my_search_app",
|
||||||
|
"description": "My first search app"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Request parameters
|
||||||
|
|
||||||
|
- `"name"`: (*Body parameter*), `string`, *Required*
|
||||||
|
The name of the search app. Must be unique and no longer than 255 characters.
|
||||||
|
- `"description"`: (*Body parameter*), `string`
|
||||||
|
A brief description of the search app.
|
||||||
|
|
||||||
|
#### Response
|
||||||
|
|
||||||
|
Success:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"data": {
|
||||||
|
"search_id": "b330ec2e91ec11efbc510242ac120006"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Failure:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 102,
|
||||||
|
"message": "Search name can't be empty."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### List search apps
|
||||||
|
|
||||||
|
**GET** `/api/v1/searches?keywords={keywords}&page={page}&page_size={page_size}&orderby={orderby}&desc={desc}&owner_ids={owner_ids}`
|
||||||
|
|
||||||
|
Lists search apps for the current user.
|
||||||
|
|
||||||
|
#### Request
|
||||||
|
|
||||||
|
- Method: GET
|
||||||
|
- URL: `/api/v1/searches`
|
||||||
|
- Headers:
|
||||||
|
- `'Authorization: Bearer <YOUR_API_KEY>'`
|
||||||
|
|
||||||
|
##### Request example
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl --request GET \
|
||||||
|
--url 'http://{address}/api/v1/searches?page=1&page_size=20' \
|
||||||
|
--header 'Authorization: Bearer <YOUR_API_KEY>'
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Request parameters
|
||||||
|
|
||||||
|
- `keywords`: (*Filter parameter*), `string`
|
||||||
|
Search keyword to filter search apps by name.
|
||||||
|
- `page`: (*Filter parameter*), `integer`
|
||||||
|
Specifies the page number. Defaults to `0` (no pagination).
|
||||||
|
- `page_size`: (*Filter parameter*), `integer`
|
||||||
|
The number of items per page. Defaults to `0` (no pagination).
|
||||||
|
- `orderby`: (*Filter parameter*), `string`
|
||||||
|
The field to sort by. Defaults to `create_time`.
|
||||||
|
- `desc`: (*Filter parameter*), `boolean`
|
||||||
|
Whether to sort in descending order. Defaults to `true`.
|
||||||
|
- `owner_ids`: (*Filter parameter*), `string` (repeatable)
|
||||||
|
Filter by owner tenant IDs. Can be specified multiple times: `?owner_ids=id1&owner_ids=id2`.
|
||||||
|
|
||||||
|
#### Response
|
||||||
|
|
||||||
|
Success:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"data": {
|
||||||
|
"total": 2,
|
||||||
|
"search_apps": [
|
||||||
|
{
|
||||||
|
"id": "b330ec2e91ec11efbc510242ac120006",
|
||||||
|
"name": "my_search_app",
|
||||||
|
"description": "My first search app",
|
||||||
|
"tenant_id": "7c8983badede11f083f184ba59bc53c7",
|
||||||
|
"create_time": 1729763127646
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Get search app
|
||||||
|
|
||||||
|
**GET** `/api/v1/searches/{search_id}`
|
||||||
|
|
||||||
|
Gets the details of a search app.
|
||||||
|
|
||||||
|
#### Request
|
||||||
|
|
||||||
|
- Method: GET
|
||||||
|
- URL: `/api/v1/searches/{search_id}`
|
||||||
|
- Headers:
|
||||||
|
- `'Authorization: Bearer <YOUR_API_KEY>'`
|
||||||
|
|
||||||
|
##### Request example
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl --request GET \
|
||||||
|
--url 'http://{address}/api/v1/searches/b330ec2e91ec11efbc510242ac120006' \
|
||||||
|
--header 'Authorization: Bearer <YOUR_API_KEY>'
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Request parameters
|
||||||
|
|
||||||
|
- `search_id`: (*Path parameter*), `string`, *Required*
|
||||||
|
The ID of the search app to retrieve.
|
||||||
|
|
||||||
|
#### Response
|
||||||
|
|
||||||
|
Success:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"data": {
|
||||||
|
"id": "b330ec2e91ec11efbc510242ac120006",
|
||||||
|
"name": "my_search_app",
|
||||||
|
"description": "My first search app",
|
||||||
|
"tenant_id": "7c8983badede11f083f184ba59bc53c7",
|
||||||
|
"search_config": {},
|
||||||
|
"create_time": 1729763127646
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Failure:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 102,
|
||||||
|
"message": "Can't find this Search App!"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Update search app
|
||||||
|
|
||||||
|
**PUT** `/api/v1/searches/{search_id}`
|
||||||
|
|
||||||
|
Updates a search app.
|
||||||
|
|
||||||
|
#### Request
|
||||||
|
|
||||||
|
- Method: PUT
|
||||||
|
- URL: `/api/v1/searches/{search_id}`
|
||||||
|
- Headers:
|
||||||
|
- `'Content-Type: application/json'`
|
||||||
|
- `'Authorization: Bearer <YOUR_API_KEY>'`
|
||||||
|
- Body:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "updated_name",
|
||||||
|
"search_config": {"top_k": 5}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Request example
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl --request PUT \
|
||||||
|
--url 'http://{address}/api/v1/searches/b330ec2e91ec11efbc510242ac120006' \
|
||||||
|
--header 'Authorization: Bearer <YOUR_API_KEY>' \
|
||||||
|
--header 'Content-Type: application/json' \
|
||||||
|
--data '{
|
||||||
|
"name": "updated_name",
|
||||||
|
"search_config": {"top_k": 5}
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Request parameters
|
||||||
|
|
||||||
|
- `search_id`: (*Path parameter*), `string`, *Required*
|
||||||
|
The ID of the search app to update.
|
||||||
|
- `"name"`: (*Body parameter*), `string`, *Required*
|
||||||
|
The new name of the search app.
|
||||||
|
- `"search_config"`: (*Body parameter*), `object`, *Required*
|
||||||
|
Configuration fields to update. Merged with the existing config.
|
||||||
|
|
||||||
|
#### Response
|
||||||
|
|
||||||
|
Success:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"data": {
|
||||||
|
"id": "b330ec2e91ec11efbc510242ac120006",
|
||||||
|
"name": "updated_name",
|
||||||
|
"search_config": {"top_k": 5},
|
||||||
|
"create_time": 1729763127646
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Failure:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 109,
|
||||||
|
"message": "No authorization."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Delete search app
|
||||||
|
|
||||||
|
**DELETE** `/api/v1/searches/{search_id}`
|
||||||
|
|
||||||
|
Deletes a search app.
|
||||||
|
|
||||||
|
#### Request
|
||||||
|
|
||||||
|
- Method: DELETE
|
||||||
|
- URL: `/api/v1/searches/{search_id}`
|
||||||
|
- Headers:
|
||||||
|
- `'Authorization: Bearer <YOUR_API_KEY>'`
|
||||||
|
|
||||||
|
##### Request example
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl --request DELETE \
|
||||||
|
--url 'http://{address}/api/v1/searches/b330ec2e91ec11efbc510242ac120006' \
|
||||||
|
--header 'Authorization: Bearer <YOUR_API_KEY>'
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Request parameters
|
||||||
|
|
||||||
|
- `search_id`: (*Path parameter*), `string`, *Required*
|
||||||
|
The ID of the search app to delete.
|
||||||
|
|
||||||
|
#### Response
|
||||||
|
|
||||||
|
Success:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"data": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Failure:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 109,
|
||||||
|
"message": "No authorization."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|||||||
@ -379,7 +379,7 @@ def _select_first_dataset_and_save(
|
|||||||
return isinstance(kb_ids, list) and len(kb_ids) > 0
|
return isinstance(kb_ids, list) and len(kb_ids) > 0
|
||||||
|
|
||||||
response_url_pattern = (
|
response_url_pattern = (
|
||||||
"/dialog/set" if save_testid == "chat-settings-save" else "/search/update"
|
"/dialog/set" if save_testid == "chat-settings-save" else "/api/v1/searches/"
|
||||||
)
|
)
|
||||||
last_payload = {}
|
last_payload = {}
|
||||||
last_combobox_text = ""
|
last_combobox_text = ""
|
||||||
|
|||||||
@ -39,7 +39,7 @@ API_APP_URL = f"/{VERSION}/api"
|
|||||||
SYSTEM_APP_URL = f"/{VERSION}/system"
|
SYSTEM_APP_URL = f"/{VERSION}/system"
|
||||||
LLM_APP_URL = f"/{VERSION}/llm"
|
LLM_APP_URL = f"/{VERSION}/llm"
|
||||||
PLUGIN_APP_URL = f"/{VERSION}/plugin"
|
PLUGIN_APP_URL = f"/{VERSION}/plugin"
|
||||||
SEARCH_APP_URL = f"/{VERSION}/search"
|
SEARCHES_URL = f"/api/{VERSION}/searches"
|
||||||
|
|
||||||
|
|
||||||
def _http_debug_enabled():
|
def _http_debug_enabled():
|
||||||
@ -142,29 +142,27 @@ def plugin_llm_tools(auth, params=None, *, headers=HEADERS):
|
|||||||
|
|
||||||
# SEARCH APP
|
# SEARCH APP
|
||||||
def search_create(auth, payload=None, *, headers=HEADERS, data=None):
|
def search_create(auth, payload=None, *, headers=HEADERS, data=None):
|
||||||
res = requests.post(url=f"{HOST_ADDRESS}{SEARCH_APP_URL}/create", headers=headers, auth=auth, json=payload, data=data)
|
res = requests.post(url=f"{HOST_ADDRESS}{SEARCHES_URL}", headers=headers, auth=auth, json=payload, data=data)
|
||||||
return res.json()
|
return res.json()
|
||||||
|
|
||||||
|
|
||||||
def search_update(auth, payload=None, *, headers=HEADERS, data=None):
|
def search_update(auth, search_id, payload=None, *, headers=HEADERS, data=None):
|
||||||
res = requests.post(url=f"{HOST_ADDRESS}{SEARCH_APP_URL}/update", headers=headers, auth=auth, json=payload, data=data)
|
res = requests.put(url=f"{HOST_ADDRESS}{SEARCHES_URL}/{search_id}", headers=headers, auth=auth, json=payload, data=data)
|
||||||
return res.json()
|
return res.json()
|
||||||
|
|
||||||
|
|
||||||
def search_detail(auth, params=None, *, headers=HEADERS):
|
def search_detail(auth, search_id, *, headers=HEADERS):
|
||||||
res = requests.get(url=f"{HOST_ADDRESS}{SEARCH_APP_URL}/detail", headers=headers, auth=auth, params=params)
|
res = requests.get(url=f"{HOST_ADDRESS}{SEARCHES_URL}/{search_id}", headers=headers, auth=auth)
|
||||||
return res.json()
|
return res.json()
|
||||||
|
|
||||||
|
|
||||||
def search_list(auth, params=None, payload=None, *, headers=HEADERS, data=None):
|
def search_list(auth, params=None, *, headers=HEADERS):
|
||||||
if payload is None:
|
res = requests.get(url=f"{HOST_ADDRESS}{SEARCHES_URL}", headers=headers, auth=auth, params=params)
|
||||||
payload = {}
|
|
||||||
res = requests.post(url=f"{HOST_ADDRESS}{SEARCH_APP_URL}/list", headers=headers, auth=auth, params=params, json=payload, data=data)
|
|
||||||
return res.json()
|
return res.json()
|
||||||
|
|
||||||
|
|
||||||
def search_rm(auth, payload=None, *, headers=HEADERS, data=None):
|
def search_rm(auth, search_id, *, headers=HEADERS):
|
||||||
res = requests.post(url=f"{HOST_ADDRESS}{SEARCH_APP_URL}/rm", headers=headers, auth=auth, json=payload, data=data)
|
res = requests.delete(url=f"{HOST_ADDRESS}{SEARCHES_URL}/{search_id}", headers=headers, auth=auth)
|
||||||
return res.json()
|
return res.json()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -31,15 +31,6 @@ def _search_name(prefix="search"):
|
|||||||
return f"{prefix}_{uuid.uuid4().hex[:8]}"
|
return f"{prefix}_{uuid.uuid4().hex[:8]}"
|
||||||
|
|
||||||
|
|
||||||
def _find_tenant_id(WebApiAuth, search_id):
|
|
||||||
res = search_list(WebApiAuth, payload={})
|
|
||||||
assert res["code"] == 0, res
|
|
||||||
for search_app in res["data"]["search_apps"]:
|
|
||||||
if search_app.get("id") == search_id:
|
|
||||||
return search_app.get("tenant_id")
|
|
||||||
assert False, res
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def search_app(WebApiAuth):
|
def search_app(WebApiAuth):
|
||||||
name = _search_name()
|
name = _search_name()
|
||||||
@ -47,7 +38,7 @@ def search_app(WebApiAuth):
|
|||||||
assert create_res["code"] == 0, create_res
|
assert create_res["code"] == 0, create_res
|
||||||
search_id = create_res["data"]["search_id"]
|
search_id = create_res["data"]["search_id"]
|
||||||
yield search_id
|
yield search_id
|
||||||
rm_res = search_rm(WebApiAuth, {"search_id": search_id})
|
rm_res = search_rm(WebApiAuth, search_id)
|
||||||
assert rm_res["code"] == 0, rm_res
|
assert rm_res["code"] == 0, rm_res
|
||||||
assert rm_res["data"] is True, rm_res
|
assert rm_res["data"] is True, rm_res
|
||||||
|
|
||||||
@ -63,28 +54,28 @@ class TestAuthorization:
|
|||||||
@pytest.mark.p2
|
@pytest.mark.p2
|
||||||
@pytest.mark.parametrize("invalid_auth, expected_code, expected_fragment", INVALID_AUTH_CASES)
|
@pytest.mark.parametrize("invalid_auth, expected_code, expected_fragment", INVALID_AUTH_CASES)
|
||||||
def test_auth_invalid_list(self, invalid_auth, expected_code, expected_fragment):
|
def test_auth_invalid_list(self, invalid_auth, expected_code, expected_fragment):
|
||||||
res = search_list(invalid_auth, payload={})
|
res = search_list(invalid_auth)
|
||||||
assert res["code"] == expected_code, res
|
assert res["code"] == expected_code, res
|
||||||
assert expected_fragment in res["message"], res
|
assert expected_fragment in res["message"], res
|
||||||
|
|
||||||
@pytest.mark.p2
|
@pytest.mark.p2
|
||||||
@pytest.mark.parametrize("invalid_auth, expected_code, expected_fragment", INVALID_AUTH_CASES)
|
@pytest.mark.parametrize("invalid_auth, expected_code, expected_fragment", INVALID_AUTH_CASES)
|
||||||
def test_auth_invalid_detail(self, invalid_auth, expected_code, expected_fragment):
|
def test_auth_invalid_detail(self, invalid_auth, expected_code, expected_fragment):
|
||||||
res = search_detail(invalid_auth, {"search_id": "dummy_search_id"})
|
res = search_detail(invalid_auth, "dummy_search_id")
|
||||||
assert res["code"] == expected_code, res
|
assert res["code"] == expected_code, res
|
||||||
assert expected_fragment in res["message"], res
|
assert expected_fragment in res["message"], res
|
||||||
|
|
||||||
@pytest.mark.p2
|
@pytest.mark.p2
|
||||||
@pytest.mark.parametrize("invalid_auth, expected_code, expected_fragment", INVALID_AUTH_CASES)
|
@pytest.mark.parametrize("invalid_auth, expected_code, expected_fragment", INVALID_AUTH_CASES)
|
||||||
def test_auth_invalid_update(self, invalid_auth, expected_code, expected_fragment):
|
def test_auth_invalid_update(self, invalid_auth, expected_code, expected_fragment):
|
||||||
res = search_update(invalid_auth, {"search_id": "dummy", "name": "dummy", "search_config": {}, "tenant_id": "dummy"})
|
res = search_update(invalid_auth, "dummy", {"name": "dummy", "search_config": {}})
|
||||||
assert res["code"] == expected_code, res
|
assert res["code"] == expected_code, res
|
||||||
assert expected_fragment in res["message"], res
|
assert expected_fragment in res["message"], res
|
||||||
|
|
||||||
@pytest.mark.p2
|
@pytest.mark.p2
|
||||||
@pytest.mark.parametrize("invalid_auth, expected_code, expected_fragment", INVALID_AUTH_CASES)
|
@pytest.mark.parametrize("invalid_auth, expected_code, expected_fragment", INVALID_AUTH_CASES)
|
||||||
def test_auth_invalid_rm(self, invalid_auth, expected_code, expected_fragment):
|
def test_auth_invalid_rm(self, invalid_auth, expected_code, expected_fragment):
|
||||||
res = search_rm(invalid_auth, {"search_id": "dummy_search_id"})
|
res = search_rm(invalid_auth, "dummy_search_id")
|
||||||
assert res["code"] == expected_code, res
|
assert res["code"] == expected_code, res
|
||||||
assert expected_fragment in res["message"], res
|
assert expected_fragment in res["message"], res
|
||||||
|
|
||||||
@ -97,33 +88,26 @@ class TestSearchCrud:
|
|||||||
assert create_res["code"] == 0, create_res
|
assert create_res["code"] == 0, create_res
|
||||||
search_id = create_res["data"]["search_id"]
|
search_id = create_res["data"]["search_id"]
|
||||||
|
|
||||||
rm_res = search_rm(WebApiAuth, {"search_id": search_id})
|
rm_res = search_rm(WebApiAuth, search_id)
|
||||||
assert rm_res["code"] == 0, rm_res
|
assert rm_res["code"] == 0, rm_res
|
||||||
assert rm_res["data"] is True, rm_res
|
assert rm_res["data"] is True, rm_res
|
||||||
|
|
||||||
@pytest.mark.p2
|
@pytest.mark.p2
|
||||||
def test_list(self, WebApiAuth, search_app):
|
def test_list(self, WebApiAuth, search_app):
|
||||||
res = search_list(WebApiAuth, payload={})
|
res = search_list(WebApiAuth)
|
||||||
assert res["code"] == 0, res
|
assert res["code"] == 0, res
|
||||||
assert any(app.get("id") == search_app for app in res["data"]["search_apps"]), res
|
assert any(app.get("id") == search_app for app in res["data"]["search_apps"]), res
|
||||||
|
|
||||||
@pytest.mark.p2
|
@pytest.mark.p2
|
||||||
def test_detail(self, WebApiAuth, search_app):
|
def test_detail(self, WebApiAuth, search_app):
|
||||||
res = search_detail(WebApiAuth, {"search_id": search_app})
|
res = search_detail(WebApiAuth, search_app)
|
||||||
assert res["code"] == 0, res
|
assert res["code"] == 0, res
|
||||||
assert res["data"].get("id") == search_app, res
|
assert res["data"].get("id") == search_app, res
|
||||||
|
|
||||||
@pytest.mark.p2
|
@pytest.mark.p2
|
||||||
def test_update(self, WebApiAuth, search_app):
|
def test_update(self, WebApiAuth, search_app):
|
||||||
tenant_id = _find_tenant_id(WebApiAuth, search_app)
|
|
||||||
new_name = _search_name("updated")
|
new_name = _search_name("updated")
|
||||||
payload = {
|
res = search_update(WebApiAuth, search_app, {"name": new_name, "search_config": {"top_k": 3}})
|
||||||
"search_id": search_app,
|
|
||||||
"name": new_name,
|
|
||||||
"search_config": {"top_k": 3},
|
|
||||||
"tenant_id": tenant_id,
|
|
||||||
}
|
|
||||||
res = search_update(WebApiAuth, payload)
|
|
||||||
assert res["code"] == 0, res
|
assert res["code"] == 0, res
|
||||||
assert res["data"].get("name") == new_name, res
|
assert res["data"].get("name") == new_name, res
|
||||||
|
|
||||||
@ -138,17 +122,10 @@ class TestSearchCrud:
|
|||||||
create_res = search_create(WebApiAuth, {"name": _search_name("invalid"), "description": "test search"})
|
create_res = search_create(WebApiAuth, {"name": _search_name("invalid"), "description": "test search"})
|
||||||
assert create_res["code"] == 0, create_res
|
assert create_res["code"] == 0, create_res
|
||||||
search_id = create_res["data"]["search_id"]
|
search_id = create_res["data"]["search_id"]
|
||||||
tenant_id = _find_tenant_id(WebApiAuth, search_id)
|
|
||||||
try:
|
try:
|
||||||
payload = {
|
res = search_update(WebApiAuth, "invalid_search_id", {"name": "invalid", "search_config": {}})
|
||||||
"search_id": "invalid_search_id",
|
|
||||||
"name": "invalid",
|
|
||||||
"search_config": {},
|
|
||||||
"tenant_id": tenant_id,
|
|
||||||
}
|
|
||||||
res = search_update(WebApiAuth, payload)
|
|
||||||
assert res["code"] == 109, res
|
assert res["code"] == 109, res
|
||||||
assert "No authorization" in res["message"], res
|
assert "No authorization" in res["message"], res
|
||||||
finally:
|
finally:
|
||||||
rm_res = search_rm(WebApiAuth, {"search_id": search_id})
|
rm_res = search_rm(WebApiAuth, search_id)
|
||||||
assert rm_res["code"] == 0, rm_res
|
assert rm_res["code"] == 0, rm_res
|
||||||
|
|||||||
@ -44,6 +44,14 @@ class _Args(dict):
|
|||||||
def get(self, key, default=None):
|
def get(self, key, default=None):
|
||||||
return super().get(key, default)
|
return super().get(key, default)
|
||||||
|
|
||||||
|
def getlist(self, key):
|
||||||
|
val = self.get(key)
|
||||||
|
if val is None:
|
||||||
|
return []
|
||||||
|
if isinstance(val, list):
|
||||||
|
return val
|
||||||
|
return [val]
|
||||||
|
|
||||||
|
|
||||||
class _EnumValue:
|
class _EnumValue:
|
||||||
def __init__(self, value):
|
def __init__(self, value):
|
||||||
@ -98,7 +106,7 @@ def set_tenant_info():
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _load_search_app(monkeypatch):
|
def _load_search_api(monkeypatch):
|
||||||
repo_root = Path(__file__).resolve().parents[4]
|
repo_root = Path(__file__).resolve().parents[4]
|
||||||
|
|
||||||
quart_mod = ModuleType("quart")
|
quart_mod = ModuleType("quart")
|
||||||
@ -233,23 +241,16 @@ def _load_search_app(monkeypatch):
|
|||||||
|
|
||||||
return _decorator
|
return _decorator
|
||||||
|
|
||||||
def _not_allowed_parameters(*_params):
|
|
||||||
def _decorator(func):
|
|
||||||
return func
|
|
||||||
|
|
||||||
return _decorator
|
|
||||||
|
|
||||||
api_utils_mod.get_request_json = _default_request_json
|
api_utils_mod.get_request_json = _default_request_json
|
||||||
api_utils_mod.get_data_error_result = _get_data_error_result
|
api_utils_mod.get_data_error_result = _get_data_error_result
|
||||||
api_utils_mod.get_json_result = _get_json_result
|
api_utils_mod.get_json_result = _get_json_result
|
||||||
api_utils_mod.server_error_response = _server_error_response
|
api_utils_mod.server_error_response = _server_error_response
|
||||||
api_utils_mod.validate_request = _validate_request
|
api_utils_mod.validate_request = _validate_request
|
||||||
api_utils_mod.not_allowed_parameters = _not_allowed_parameters
|
|
||||||
monkeypatch.setitem(sys.modules, "api.utils.api_utils", api_utils_mod)
|
monkeypatch.setitem(sys.modules, "api.utils.api_utils", api_utils_mod)
|
||||||
utils_pkg.api_utils = api_utils_mod
|
utils_pkg.api_utils = api_utils_mod
|
||||||
|
|
||||||
module_name = "test_search_routes_unit_module"
|
module_name = "test_search_api_unit_module"
|
||||||
module_path = repo_root / "api" / "apps" / "search_app.py"
|
module_path = repo_root / "api" / "apps" / "restful_apis" / "search_api.py"
|
||||||
spec = importlib.util.spec_from_file_location(module_name, module_path)
|
spec = importlib.util.spec_from_file_location(module_name, module_path)
|
||||||
module = importlib.util.module_from_spec(spec)
|
module = importlib.util.module_from_spec(spec)
|
||||||
module.manager = _DummyManager()
|
module.manager = _DummyManager()
|
||||||
@ -260,7 +261,7 @@ def _load_search_app(monkeypatch):
|
|||||||
|
|
||||||
@pytest.mark.p2
|
@pytest.mark.p2
|
||||||
def test_create_route_matrix_unit(monkeypatch):
|
def test_create_route_matrix_unit(monkeypatch):
|
||||||
module = _load_search_app(monkeypatch)
|
module = _load_search_api(monkeypatch)
|
||||||
|
|
||||||
_set_request_json(monkeypatch, module, {"name": 1})
|
_set_request_json(monkeypatch, module, {"name": 1})
|
||||||
res = _run(module.create())
|
res = _run(module.create())
|
||||||
@ -308,40 +309,46 @@ def test_create_route_matrix_unit(monkeypatch):
|
|||||||
|
|
||||||
@pytest.mark.p2
|
@pytest.mark.p2
|
||||||
def test_update_and_detail_route_matrix_unit(monkeypatch):
|
def test_update_and_detail_route_matrix_unit(monkeypatch):
|
||||||
module = _load_search_app(monkeypatch)
|
module = _load_search_api(monkeypatch)
|
||||||
|
|
||||||
_set_request_json(monkeypatch, module, {"search_id": "s1", "name": 1, "search_config": {}, "tenant_id": "tenant-1"})
|
# update: name not string
|
||||||
res = _run(module.update())
|
_set_request_json(monkeypatch, module, {"name": 1, "search_config": {}})
|
||||||
|
res = _run(module.update(search_id="s1"))
|
||||||
assert res["code"] == module.RetCode.DATA_ERROR
|
assert res["code"] == module.RetCode.DATA_ERROR
|
||||||
assert "must be string" in res["message"]
|
assert "must be string" in res["message"]
|
||||||
|
|
||||||
_set_request_json(monkeypatch, module, {"search_id": "s1", "name": " ", "search_config": {}, "tenant_id": "tenant-1"})
|
# update: empty name
|
||||||
res = _run(module.update())
|
_set_request_json(monkeypatch, module, {"name": " ", "search_config": {}})
|
||||||
|
res = _run(module.update(search_id="s1"))
|
||||||
assert res["code"] == module.RetCode.DATA_ERROR
|
assert res["code"] == module.RetCode.DATA_ERROR
|
||||||
assert "empty" in res["message"].lower()
|
assert "empty" in res["message"].lower()
|
||||||
|
|
||||||
_set_request_json(monkeypatch, module, {"search_id": "s1", "name": "a" * 256, "search_config": {}, "tenant_id": "tenant-1"})
|
# update: name too long
|
||||||
res = _run(module.update())
|
_set_request_json(monkeypatch, module, {"name": "a" * 256, "search_config": {}})
|
||||||
|
res = _run(module.update(search_id="s1"))
|
||||||
assert res["code"] == module.RetCode.DATA_ERROR
|
assert res["code"] == module.RetCode.DATA_ERROR
|
||||||
assert "large than" in res["message"]
|
assert "large than" in res["message"]
|
||||||
|
|
||||||
_set_request_json(monkeypatch, module, {"search_id": "s1", "name": "ok", "search_config": {}, "tenant_id": "tenant-1"})
|
# update: tenant not found
|
||||||
|
_set_request_json(monkeypatch, module, {"name": "ok", "search_config": {}})
|
||||||
monkeypatch.setattr(module.TenantService, "get_by_id", lambda _tenant_id: (False, None))
|
monkeypatch.setattr(module.TenantService, "get_by_id", lambda _tenant_id: (False, None))
|
||||||
res = _run(module.update())
|
res = _run(module.update(search_id="s1"))
|
||||||
assert res["code"] == module.RetCode.DATA_ERROR
|
assert res["code"] == module.RetCode.DATA_ERROR
|
||||||
assert "authorized identity" in res["message"].lower()
|
assert "authorized identity" in res["message"].lower()
|
||||||
|
|
||||||
|
# update: no access
|
||||||
monkeypatch.setattr(module.TenantService, "get_by_id", lambda _tenant_id: (True, SimpleNamespace(id=_tenant_id)))
|
monkeypatch.setattr(module.TenantService, "get_by_id", lambda _tenant_id: (True, SimpleNamespace(id=_tenant_id)))
|
||||||
monkeypatch.setattr(module.SearchService, "accessible4deletion", lambda _search_id, _user_id: False)
|
monkeypatch.setattr(module.SearchService, "accessible4deletion", lambda _search_id, _user_id: False)
|
||||||
_set_request_json(monkeypatch, module, {"search_id": "s1", "name": "ok", "search_config": {}, "tenant_id": "tenant-1"})
|
_set_request_json(monkeypatch, module, {"name": "ok", "search_config": {}})
|
||||||
res = _run(module.update())
|
res = _run(module.update(search_id="s1"))
|
||||||
assert res["code"] == module.RetCode.AUTHENTICATION_ERROR
|
assert res["code"] == module.RetCode.AUTHENTICATION_ERROR
|
||||||
assert "authorization" in res["message"].lower()
|
assert "authorization" in res["message"].lower()
|
||||||
|
|
||||||
|
# update: search not found (query returns [None])
|
||||||
monkeypatch.setattr(module.SearchService, "accessible4deletion", lambda _search_id, _user_id: True)
|
monkeypatch.setattr(module.SearchService, "accessible4deletion", lambda _search_id, _user_id: True)
|
||||||
monkeypatch.setattr(module.SearchService, "query", lambda **_kwargs: [None])
|
monkeypatch.setattr(module.SearchService, "query", lambda **_kwargs: [None])
|
||||||
_set_request_json(monkeypatch, module, {"search_id": "s1", "name": "ok", "search_config": {}, "tenant_id": "tenant-1"})
|
_set_request_json(monkeypatch, module, {"name": "ok", "search_config": {}})
|
||||||
res = _run(module.update())
|
res = _run(module.update(search_id="s1"))
|
||||||
assert res["code"] == module.RetCode.DATA_ERROR
|
assert res["code"] == module.RetCode.DATA_ERROR
|
||||||
assert "cannot find search" in res["message"].lower()
|
assert "cannot find search" in res["message"].lower()
|
||||||
|
|
||||||
@ -354,18 +361,21 @@ def test_update_and_detail_route_matrix_unit(monkeypatch):
|
|||||||
return [SimpleNamespace(id="dup")]
|
return [SimpleNamespace(id="dup")]
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
# update: duplicate name
|
||||||
monkeypatch.setattr(module.SearchService, "query", _query_duplicate)
|
monkeypatch.setattr(module.SearchService, "query", _query_duplicate)
|
||||||
_set_request_json(monkeypatch, module, {"search_id": "s1", "name": "new-name", "search_config": {}, "tenant_id": "tenant-1"})
|
_set_request_json(monkeypatch, module, {"name": "new-name", "search_config": {}})
|
||||||
res = _run(module.update())
|
res = _run(module.update(search_id="s1"))
|
||||||
assert res["code"] == module.RetCode.DATA_ERROR
|
assert res["code"] == module.RetCode.DATA_ERROR
|
||||||
assert "duplicated" in res["message"].lower()
|
assert "duplicated" in res["message"].lower()
|
||||||
|
|
||||||
|
# update: search_config not a dict
|
||||||
monkeypatch.setattr(module.SearchService, "query", lambda **_kwargs: [existing])
|
monkeypatch.setattr(module.SearchService, "query", lambda **_kwargs: [existing])
|
||||||
_set_request_json(monkeypatch, module, {"search_id": "s1", "name": "old-name", "search_config": [], "tenant_id": "tenant-1"})
|
_set_request_json(monkeypatch, module, {"name": "old-name", "search_config": []})
|
||||||
res = _run(module.update())
|
res = _run(module.update(search_id="s1"))
|
||||||
assert res["code"] == module.RetCode.DATA_ERROR
|
assert res["code"] == module.RetCode.DATA_ERROR
|
||||||
assert "json object" in res["message"].lower()
|
assert "json object" in res["message"].lower()
|
||||||
|
|
||||||
|
# update: update_by_id fails, verifies config merge and field exclusion
|
||||||
captured = {}
|
captured = {}
|
||||||
|
|
||||||
def _update_fail(search_id, req):
|
def _update_fail(search_id, req):
|
||||||
@ -374,92 +384,96 @@ def test_update_and_detail_route_matrix_unit(monkeypatch):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
monkeypatch.setattr(module.SearchService, "update_by_id", _update_fail)
|
monkeypatch.setattr(module.SearchService, "update_by_id", _update_fail)
|
||||||
_set_request_json(monkeypatch, module, {"search_id": "s1", "name": "old-name", "search_config": {"top_k": 3}, "tenant_id": "tenant-1"})
|
_set_request_json(monkeypatch, module, {"name": "old-name", "search_config": {"top_k": 3}})
|
||||||
res = _run(module.update())
|
res = _run(module.update(search_id="s1"))
|
||||||
assert res["code"] == module.RetCode.DATA_ERROR
|
assert res["code"] == module.RetCode.DATA_ERROR
|
||||||
assert "failed to update" in res["message"].lower()
|
assert "failed to update" in res["message"].lower()
|
||||||
assert captured["search_id"] == "s1"
|
assert captured["search_id"] == "s1"
|
||||||
assert "search_id" not in captured["req"]
|
|
||||||
assert "tenant_id" not in captured["req"]
|
|
||||||
assert captured["req"]["search_config"] == {"existing": 1, "top_k": 3}
|
assert captured["req"]["search_config"] == {"existing": 1, "top_k": 3}
|
||||||
|
|
||||||
|
# update: get_by_id fails after successful update
|
||||||
monkeypatch.setattr(module.SearchService, "update_by_id", lambda _search_id, _req: True)
|
monkeypatch.setattr(module.SearchService, "update_by_id", lambda _search_id, _req: True)
|
||||||
monkeypatch.setattr(module.SearchService, "get_by_id", lambda _search_id: (False, None))
|
monkeypatch.setattr(module.SearchService, "get_by_id", lambda _search_id: (False, None))
|
||||||
res = _run(module.update())
|
res = _run(module.update(search_id="s1"))
|
||||||
assert res["code"] == module.RetCode.DATA_ERROR
|
assert res["code"] == module.RetCode.DATA_ERROR
|
||||||
assert "failed to fetch" in res["message"].lower()
|
assert "failed to fetch" in res["message"].lower()
|
||||||
|
|
||||||
|
# update: success
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
module.SearchService,
|
module.SearchService,
|
||||||
"get_by_id",
|
"get_by_id",
|
||||||
lambda _search_id: (True, _SearchRecord(search_id=_search_id, name="old-name", search_config={"existing": 1, "top_k": 3})),
|
lambda _search_id: (True, _SearchRecord(search_id=_search_id, name="old-name", search_config={"existing": 1, "top_k": 3})),
|
||||||
)
|
)
|
||||||
res = _run(module.update())
|
res = _run(module.update(search_id="s1"))
|
||||||
assert res["code"] == 0
|
assert res["code"] == 0
|
||||||
assert res["data"]["id"] == "s1"
|
assert res["data"]["id"] == "s1"
|
||||||
|
|
||||||
|
# update: exception
|
||||||
def _raise_query(**_kwargs):
|
def _raise_query(**_kwargs):
|
||||||
raise RuntimeError("update boom")
|
raise RuntimeError("update boom")
|
||||||
|
|
||||||
monkeypatch.setattr(module.SearchService, "query", _raise_query)
|
monkeypatch.setattr(module.SearchService, "query", _raise_query)
|
||||||
_set_request_json(monkeypatch, module, {"search_id": "s1", "name": "old-name", "search_config": {"top_k": 3}, "tenant_id": "tenant-1"})
|
_set_request_json(monkeypatch, module, {"name": "old-name", "search_config": {"top_k": 3}})
|
||||||
res = _run(module.update())
|
res = _run(module.update(search_id="s1"))
|
||||||
assert res["code"] == module.RetCode.EXCEPTION_ERROR
|
assert res["code"] == module.RetCode.EXCEPTION_ERROR
|
||||||
assert "update boom" in res["message"]
|
assert "update boom" in res["message"]
|
||||||
|
|
||||||
_set_request_args(monkeypatch, module, {"search_id": "s1"})
|
# detail: no permission
|
||||||
monkeypatch.setattr(module.UserTenantService, "query", lambda **_kwargs: [SimpleNamespace(tenant_id="tenant-a")])
|
monkeypatch.setattr(module.UserTenantService, "query", lambda **_kwargs: [SimpleNamespace(tenant_id="tenant-a")])
|
||||||
monkeypatch.setattr(module.SearchService, "query", lambda **_kwargs: [])
|
monkeypatch.setattr(module.SearchService, "query", lambda **_kwargs: [])
|
||||||
res = module.detail()
|
res = module.detail(search_id="s1")
|
||||||
assert res["code"] == module.RetCode.OPERATING_ERROR
|
assert res["code"] == module.RetCode.OPERATING_ERROR
|
||||||
assert "permission" in res["message"].lower()
|
assert "permission" in res["message"].lower()
|
||||||
|
|
||||||
|
# detail: search not found
|
||||||
monkeypatch.setattr(module.SearchService, "query", lambda **_kwargs: [SimpleNamespace(id="s1")])
|
monkeypatch.setattr(module.SearchService, "query", lambda **_kwargs: [SimpleNamespace(id="s1")])
|
||||||
monkeypatch.setattr(module.SearchService, "get_detail", lambda _search_id: None)
|
monkeypatch.setattr(module.SearchService, "get_detail", lambda _search_id: None)
|
||||||
res = module.detail()
|
res = module.detail(search_id="s1")
|
||||||
assert res["code"] == module.RetCode.DATA_ERROR
|
assert res["code"] == module.RetCode.DATA_ERROR
|
||||||
assert "can't find" in res["message"].lower()
|
assert "can't find" in res["message"].lower()
|
||||||
|
|
||||||
|
# detail: success
|
||||||
monkeypatch.setattr(module.SearchService, "get_detail", lambda _search_id: {"id": _search_id, "name": "detail-name"})
|
monkeypatch.setattr(module.SearchService, "get_detail", lambda _search_id: {"id": _search_id, "name": "detail-name"})
|
||||||
res = module.detail()
|
res = module.detail(search_id="s1")
|
||||||
assert res["code"] == 0
|
assert res["code"] == 0
|
||||||
assert res["data"]["id"] == "s1"
|
assert res["data"]["id"] == "s1"
|
||||||
|
|
||||||
|
# detail: exception
|
||||||
def _raise_detail(_search_id):
|
def _raise_detail(_search_id):
|
||||||
raise RuntimeError("detail boom")
|
raise RuntimeError("detail boom")
|
||||||
|
|
||||||
monkeypatch.setattr(module.SearchService, "get_detail", _raise_detail)
|
monkeypatch.setattr(module.SearchService, "get_detail", _raise_detail)
|
||||||
res = module.detail()
|
res = module.detail(search_id="s1")
|
||||||
assert res["code"] == module.RetCode.EXCEPTION_ERROR
|
assert res["code"] == module.RetCode.EXCEPTION_ERROR
|
||||||
assert "detail boom" in res["message"]
|
assert "detail boom" in res["message"]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.p2
|
@pytest.mark.p2
|
||||||
def test_list_and_rm_route_matrix_unit(monkeypatch):
|
def test_list_and_delete_route_matrix_unit(monkeypatch):
|
||||||
module = _load_search_app(monkeypatch)
|
module = _load_search_api(monkeypatch)
|
||||||
|
|
||||||
|
# list: no owner_ids, with pagination
|
||||||
_set_request_args(
|
_set_request_args(
|
||||||
monkeypatch,
|
monkeypatch,
|
||||||
module,
|
module,
|
||||||
{"keywords": "k", "page": "1", "page_size": "2", "orderby": "create_time", "desc": "false"},
|
{"keywords": "k", "page": "1", "page_size": "2", "orderby": "create_time", "desc": "false"},
|
||||||
)
|
)
|
||||||
_set_request_json(monkeypatch, module, {"owner_ids": []})
|
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
module.SearchService,
|
module.SearchService,
|
||||||
"get_by_tenant_ids",
|
"get_by_tenant_ids",
|
||||||
lambda _tenants, _uid, _page, _size, _orderby, _desc, _keywords: ([{"id": "a", "tenant_id": "tenant-1"}], 1),
|
lambda _tenants, _uid, _page, _size, _orderby, _desc, _keywords: ([{"id": "a", "tenant_id": "tenant-1"}], 1),
|
||||||
)
|
)
|
||||||
res = _run(module.list_search_app())
|
res = module.list_searches()
|
||||||
assert res["code"] == 0
|
assert res["code"] == 0
|
||||||
assert res["data"]["total"] == 1
|
assert res["data"]["total"] == 1
|
||||||
assert res["data"]["search_apps"][0]["id"] == "a"
|
assert res["data"]["search_apps"][0]["id"] == "a"
|
||||||
|
|
||||||
|
# list: with owner_ids filter and pagination
|
||||||
_set_request_args(
|
_set_request_args(
|
||||||
monkeypatch,
|
monkeypatch,
|
||||||
module,
|
module,
|
||||||
{"keywords": "k", "page": "1", "page_size": "1", "orderby": "create_time", "desc": "true"},
|
{"keywords": "k", "page": "1", "page_size": "1", "orderby": "create_time", "desc": "true", "owner_ids": ["tenant-1"]},
|
||||||
)
|
)
|
||||||
_set_request_json(monkeypatch, module, {"owner_ids": ["tenant-1"]})
|
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
module.SearchService,
|
module.SearchService,
|
||||||
"get_by_tenant_ids",
|
"get_by_tenant_ids",
|
||||||
@ -468,42 +482,46 @@ def test_list_and_rm_route_matrix_unit(monkeypatch):
|
|||||||
2,
|
2,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
res = _run(module.list_search_app())
|
res = module.list_searches()
|
||||||
assert res["code"] == 0
|
assert res["code"] == 0
|
||||||
assert res["data"]["total"] == 1
|
assert res["data"]["total"] == 1
|
||||||
assert len(res["data"]["search_apps"]) == 1
|
assert len(res["data"]["search_apps"]) == 1
|
||||||
assert res["data"]["search_apps"][0]["tenant_id"] == "tenant-1"
|
assert res["data"]["search_apps"][0]["tenant_id"] == "tenant-1"
|
||||||
|
|
||||||
|
# list: exception
|
||||||
def _raise_list(*_args, **_kwargs):
|
def _raise_list(*_args, **_kwargs):
|
||||||
raise RuntimeError("list boom")
|
raise RuntimeError("list boom")
|
||||||
|
|
||||||
monkeypatch.setattr(module.SearchService, "get_by_tenant_ids", _raise_list)
|
monkeypatch.setattr(module.SearchService, "get_by_tenant_ids", _raise_list)
|
||||||
_set_request_json(monkeypatch, module, {"owner_ids": []})
|
_set_request_args(monkeypatch, module, {})
|
||||||
res = _run(module.list_search_app())
|
res = module.list_searches()
|
||||||
assert res["code"] == module.RetCode.EXCEPTION_ERROR
|
assert res["code"] == module.RetCode.EXCEPTION_ERROR
|
||||||
assert "list boom" in res["message"]
|
assert "list boom" in res["message"]
|
||||||
|
|
||||||
_set_request_json(monkeypatch, module, {"search_id": "search-1"})
|
# delete: no authorization
|
||||||
monkeypatch.setattr(module.SearchService, "accessible4deletion", lambda _search_id, _user_id: False)
|
monkeypatch.setattr(module.SearchService, "accessible4deletion", lambda _search_id, _user_id: False)
|
||||||
res = _run(module.rm())
|
res = module.delete_search(search_id="search-1")
|
||||||
assert res["code"] == module.RetCode.AUTHENTICATION_ERROR
|
assert res["code"] == module.RetCode.AUTHENTICATION_ERROR
|
||||||
assert "authorization" in res["message"].lower()
|
assert "authorization" in res["message"].lower()
|
||||||
|
|
||||||
|
# delete: delete_by_id fails
|
||||||
monkeypatch.setattr(module.SearchService, "accessible4deletion", lambda _search_id, _user_id: True)
|
monkeypatch.setattr(module.SearchService, "accessible4deletion", lambda _search_id, _user_id: True)
|
||||||
monkeypatch.setattr(module.SearchService, "delete_by_id", lambda _search_id: False)
|
monkeypatch.setattr(module.SearchService, "delete_by_id", lambda _search_id: False)
|
||||||
res = _run(module.rm())
|
res = module.delete_search(search_id="search-1")
|
||||||
assert res["code"] == module.RetCode.DATA_ERROR
|
assert res["code"] == module.RetCode.DATA_ERROR
|
||||||
assert "failed to delete" in res["message"].lower()
|
assert "failed to delete" in res["message"].lower()
|
||||||
|
|
||||||
|
# delete: success
|
||||||
monkeypatch.setattr(module.SearchService, "delete_by_id", lambda _search_id: True)
|
monkeypatch.setattr(module.SearchService, "delete_by_id", lambda _search_id: True)
|
||||||
res = _run(module.rm())
|
res = module.delete_search(search_id="search-1")
|
||||||
assert res["code"] == 0
|
assert res["code"] == 0
|
||||||
assert res["data"] is True
|
assert res["data"] is True
|
||||||
|
|
||||||
|
# delete: exception
|
||||||
def _raise_delete(_search_id):
|
def _raise_delete(_search_id):
|
||||||
raise RuntimeError("rm boom")
|
raise RuntimeError("rm boom")
|
||||||
|
|
||||||
monkeypatch.setattr(module.SearchService, "delete_by_id", _raise_delete)
|
monkeypatch.setattr(module.SearchService, "delete_by_id", _raise_delete)
|
||||||
res = _run(module.rm())
|
res = module.delete_search(search_id="search-1")
|
||||||
assert res["code"] == module.RetCode.EXCEPTION_ERROR
|
assert res["code"] == module.RetCode.EXCEPTION_ERROR
|
||||||
assert "rm boom" in res["message"]
|
assert "rm boom" in res["message"]
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import message from '@/components/ui/message';
|
|||||||
import { useSetModalState } from '@/hooks/common-hooks';
|
import { useSetModalState } from '@/hooks/common-hooks';
|
||||||
import { useHandleSearchChange } from '@/hooks/logic-hooks';
|
import { useHandleSearchChange } from '@/hooks/logic-hooks';
|
||||||
import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
|
import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
|
||||||
import searchService, { searchServiceNext } from '@/services/search-service';
|
import searchService from '@/services/search-service';
|
||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
import { useDebounce } from 'ahooks';
|
import { useDebounce } from 'ahooks';
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
@ -103,14 +103,13 @@ export const useFetchSearchList = () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const { data: response } = await searchServiceNext.getSearchList(
|
const { data: response } = await searchService.getSearchList(
|
||||||
{
|
{
|
||||||
params: {
|
params: {
|
||||||
keywords: debouncedSearchString,
|
keywords: debouncedSearchString,
|
||||||
page_size: pagination.pageSize,
|
page_size: pagination.pageSize,
|
||||||
page: pagination.current,
|
page: pagination.current,
|
||||||
},
|
},
|
||||||
data: {},
|
|
||||||
},
|
},
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
@ -203,24 +202,21 @@ export const useFetchSearchDetail = (tenantId?: string) => {
|
|||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const shared_id = searchParams.get('shared_id');
|
const shared_id = searchParams.get('shared_id');
|
||||||
const searchId = id || shared_id;
|
const searchId = id || shared_id;
|
||||||
let param: { search_id: string | null; tenant_id?: string } = {
|
|
||||||
search_id: searchId,
|
|
||||||
};
|
|
||||||
if (shared_id) {
|
|
||||||
param = {
|
|
||||||
search_id: searchId,
|
|
||||||
tenant_id: tenantId,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const fetchSearchDetailFunc = shared_id
|
|
||||||
? searchService.getSearchDetailShare
|
|
||||||
: searchService.getSearchDetail;
|
|
||||||
|
|
||||||
const { data, isLoading, isError } = useQuery<SearchDetailResponse, Error>({
|
const { data, isLoading, isError } = useQuery<SearchDetailResponse, Error>({
|
||||||
queryKey: ['searchDetail', searchId],
|
queryKey: ['searchDetail', searchId],
|
||||||
enabled: !shared_id || !!tenantId,
|
enabled: !shared_id || !!tenantId,
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const { data: response } = await fetchSearchDetailFunc(param);
|
let res;
|
||||||
|
if (shared_id) {
|
||||||
|
res = await searchService.getSearchDetailShare(
|
||||||
|
{ params: { search_id: searchId, tenant_id: tenantId } },
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
res = await searchService.getSearchDetail({ search_id: searchId });
|
||||||
|
}
|
||||||
|
const response = res.data;
|
||||||
if (response.code !== 0) {
|
if (response.code !== 0) {
|
||||||
throw new Error(response.message || 'Failed to fetch search detail');
|
throw new Error(response.message || 'Failed to fetch search detail');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import api from '@/utils/api';
|
import api from '@/utils/api';
|
||||||
import registerServer, { registerNextServer } from '@/utils/register-server';
|
import { registerNextServer } from '@/utils/register-server';
|
||||||
import request from '@/utils/request';
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
createSearch,
|
createSearch,
|
||||||
@ -13,6 +12,7 @@ const {
|
|||||||
getRelatedQuestionsShare,
|
getRelatedQuestionsShare,
|
||||||
getSearchDetailShare,
|
getSearchDetailShare,
|
||||||
} = api;
|
} = api;
|
||||||
|
|
||||||
const methods = {
|
const methods = {
|
||||||
createSearch: {
|
createSearch: {
|
||||||
url: createSearch,
|
url: createSearch,
|
||||||
@ -20,16 +20,16 @@ const methods = {
|
|||||||
},
|
},
|
||||||
getSearchList: {
|
getSearchList: {
|
||||||
url: getSearchList,
|
url: getSearchList,
|
||||||
method: 'post',
|
method: 'get',
|
||||||
},
|
},
|
||||||
deleteSearch: { url: deleteSearch, method: 'post' },
|
deleteSearch: { url: deleteSearch, method: 'delete' },
|
||||||
getSearchDetail: {
|
getSearchDetail: {
|
||||||
url: getSearchDetail,
|
url: getSearchDetail,
|
||||||
method: 'get',
|
method: 'get',
|
||||||
},
|
},
|
||||||
updateSearchSetting: {
|
updateSearchSetting: {
|
||||||
url: updateSearchSetting,
|
url: updateSearchSetting,
|
||||||
method: 'post',
|
method: 'put',
|
||||||
},
|
},
|
||||||
askShare: {
|
askShare: {
|
||||||
url: askShare,
|
url: askShare,
|
||||||
@ -43,14 +43,13 @@ const methods = {
|
|||||||
url: getRelatedQuestionsShare,
|
url: getRelatedQuestionsShare,
|
||||||
method: 'post',
|
method: 'post',
|
||||||
},
|
},
|
||||||
|
|
||||||
getSearchDetailShare: {
|
getSearchDetailShare: {
|
||||||
url: getSearchDetailShare,
|
url: getSearchDetailShare,
|
||||||
method: 'get',
|
method: 'get',
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
const searchService = registerServer<keyof typeof methods>(methods, request);
|
|
||||||
export const searchServiceNext =
|
const searchService = registerNextServer<keyof typeof methods>(methods);
|
||||||
registerNextServer<keyof typeof methods>(methods);
|
export const searchServiceNext = searchService;
|
||||||
|
|
||||||
export default searchService;
|
export default searchService;
|
||||||
|
|||||||
@ -237,12 +237,15 @@ export default {
|
|||||||
testMcpServer: `${api_host}/mcp_server/test_mcp`,
|
testMcpServer: `${api_host}/mcp_server/test_mcp`,
|
||||||
|
|
||||||
// next-search
|
// next-search
|
||||||
createSearch: `${api_host}/search/create`,
|
createSearch: `${ExternalApi}${api_host}/searches`,
|
||||||
getSearchList: `${api_host}/search/list`,
|
getSearchList: `${ExternalApi}${api_host}/searches`,
|
||||||
deleteSearch: `${api_host}/search/rm`,
|
deleteSearch: (params: { search_id: string }) =>
|
||||||
getSearchDetail: `${api_host}/search/detail`,
|
`${ExternalApi}${api_host}/searches/${params.search_id}`,
|
||||||
|
getSearchDetail: (params: { search_id: string }) =>
|
||||||
|
`${ExternalApi}${api_host}/searches/${params.search_id}`,
|
||||||
getSearchDetailShare: `${ExternalApi}${api_host}/searchbots/detail`,
|
getSearchDetailShare: `${ExternalApi}${api_host}/searchbots/detail`,
|
||||||
updateSearchSetting: `${api_host}/search/update`,
|
updateSearchSetting: (params: { search_id: string }) =>
|
||||||
|
`${ExternalApi}${api_host}/searches/${params.search_id}`,
|
||||||
askShare: `${ExternalApi}${api_host}/searchbots/ask`,
|
askShare: `${ExternalApi}${api_host}/searchbots/ask`,
|
||||||
mindmapShare: `${ExternalApi}${api_host}/searchbots/mindmap`,
|
mindmapShare: `${ExternalApi}${api_host}/searchbots/mindmap`,
|
||||||
getRelatedQuestionsShare: `${ExternalApi}${api_host}/searchbots/related_questions`,
|
getRelatedQuestionsShare: `${ExternalApi}${api_host}/searchbots/related_questions`,
|
||||||
|
|||||||
@ -82,7 +82,7 @@ const API_WHITELIST = [
|
|||||||
'/v1/dialog/set',
|
'/v1/dialog/set',
|
||||||
'/v1/canvas/set',
|
'/v1/canvas/set',
|
||||||
'/v1/canvas/setting',
|
'/v1/canvas/setting',
|
||||||
'/v1/search/update',
|
'/api/v1/searches/',
|
||||||
'/api/v1/memories',
|
'/api/v1/memories',
|
||||||
'/v1/kb/create',
|
'/v1/kb/create',
|
||||||
'/v1/kb/update',
|
'/v1/kb/update',
|
||||||
|
|||||||
Reference in New Issue
Block a user