mirror of
https://github.com/khoj-ai/khoj.git
synced 2024-11-23 15:38:55 +01:00
Split /api/v1/index/update into /api/content PUT, PATCH API endpoints
- This utilizes PUT, PATCH HTTP method semantics to remove need for the "regenerate" query param and "/update" url suffix - This should make the url more succinct and API request intent more understandable by using existing HTTP method semantics
This commit is contained in:
parent
65dade4838
commit
5923b6d89e
10 changed files with 76 additions and 37 deletions
|
@ -233,11 +233,15 @@ function pushDataToKhoj (regenerate = false) {
|
||||||
|
|
||||||
// Request indexing files on server. With upto 1000 files in each request
|
// Request indexing files on server. With upto 1000 files in each request
|
||||||
for (let i = 0; i < filesDataToPush.length; i += 1000) {
|
for (let i = 0; i < filesDataToPush.length; i += 1000) {
|
||||||
|
const syncUrl = `${hostURL}/api/content?client=desktop`;
|
||||||
const filesDataGroup = filesDataToPush.slice(i, i + 1000);
|
const filesDataGroup = filesDataToPush.slice(i, i + 1000);
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
filesDataGroup.forEach(fileData => { formData.append('files', fileData.blob, fileData.path) });
|
filesDataGroup.forEach(fileData => { formData.append('files', fileData.blob, fileData.path) });
|
||||||
let request = axios.post(`${hostURL}/api/v1/index/update?force=${regenerate}&client=desktop`, formData, { headers });
|
requests.push(
|
||||||
requests.push(request);
|
regenerate
|
||||||
|
? axios.put(syncUrl, formData, { headers })
|
||||||
|
: axios.patch(syncUrl, formData, { headers })
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for requests batch to finish
|
// Wait for requests batch to finish
|
||||||
|
|
|
@ -424,12 +424,12 @@ Auto invokes setup steps on calling main entrypoint."
|
||||||
"Send multi-part form `BODY' of `CONTENT-TYPE' in request to khoj server.
|
"Send multi-part form `BODY' of `CONTENT-TYPE' in request to khoj server.
|
||||||
Append 'TYPE-QUERY' as query parameter in request url.
|
Append 'TYPE-QUERY' as query parameter in request url.
|
||||||
Specify `BOUNDARY' used to separate files in request header."
|
Specify `BOUNDARY' used to separate files in request header."
|
||||||
(let ((url-request-method "POST")
|
(let ((url-request-method ((if force) "PUT" "PATCH"))
|
||||||
(url-request-data body)
|
(url-request-data body)
|
||||||
(url-request-extra-headers `(("content-type" . ,(format "multipart/form-data; boundary=%s" boundary))
|
(url-request-extra-headers `(("content-type" . ,(format "multipart/form-data; boundary=%s" boundary))
|
||||||
("Authorization" . ,(format "Bearer %s" khoj-api-key)))))
|
("Authorization" . ,(format "Bearer %s" khoj-api-key)))))
|
||||||
(with-current-buffer
|
(with-current-buffer
|
||||||
(url-retrieve (format "%s/api/v1/index/update?%s&force=%s&client=emacs" khoj-server-url type-query (or force "false"))
|
(url-retrieve (format "%s/api/content?%s&client=emacs" khoj-server-url type-query)
|
||||||
;; render response from indexing API endpoint on server
|
;; render response from indexing API endpoint on server
|
||||||
(lambda (status)
|
(lambda (status)
|
||||||
(if (not (plist-get status :error))
|
(if (not (plist-get status :error))
|
||||||
|
|
|
@ -89,10 +89,11 @@ export async function updateContentIndex(vault: Vault, setting: KhojSetting, las
|
||||||
for (let i = 0; i < fileData.length; i += 1000) {
|
for (let i = 0; i < fileData.length; i += 1000) {
|
||||||
const filesGroup = fileData.slice(i, i + 1000);
|
const filesGroup = fileData.slice(i, i + 1000);
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
|
const method = regenerate ? "PUT" : "PATCH";
|
||||||
filesGroup.forEach(fileItem => { formData.append('files', fileItem.blob, fileItem.path) });
|
filesGroup.forEach(fileItem => { formData.append('files', fileItem.blob, fileItem.path) });
|
||||||
// Call Khoj backend to update index with all markdown, pdf files
|
// Call Khoj backend to update index with all markdown, pdf files
|
||||||
const response = await fetch(`${setting.khojUrl}/api/v1/index/update?force=${regenerate}&client=obsidian`, {
|
const response = await fetch(`${setting.khojUrl}/api/content?client=obsidian`, {
|
||||||
method: 'POST',
|
method: method,
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${setting.khojApiKey}`,
|
'Authorization': `Bearer ${setting.khojApiKey}`,
|
||||||
},
|
},
|
||||||
|
|
|
@ -275,8 +275,8 @@ export function uploadDataForIndexing(
|
||||||
// Wait for all files to be read before making the fetch request
|
// Wait for all files to be read before making the fetch request
|
||||||
Promise.all(fileReadPromises)
|
Promise.all(fileReadPromises)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
return fetch("/api/v1/index/update?force=false&client=web", {
|
return fetch("/api/content?client=web", {
|
||||||
method: "POST",
|
method: "PATCH",
|
||||||
body: formData,
|
body: formData,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
|
|
@ -42,7 +42,7 @@ from khoj.database.adapters import (
|
||||||
)
|
)
|
||||||
from khoj.database.models import ClientApplication, KhojUser, ProcessLock, Subscription
|
from khoj.database.models import ClientApplication, KhojUser, ProcessLock, Subscription
|
||||||
from khoj.processor.embeddings import CrossEncoderModel, EmbeddingsModel
|
from khoj.processor.embeddings import CrossEncoderModel, EmbeddingsModel
|
||||||
from khoj.routers.indexer import configure_content, configure_search
|
from khoj.routers.api_content import configure_content, configure_search
|
||||||
from khoj.routers.twilio import is_twilio_enabled
|
from khoj.routers.twilio import is_twilio_enabled
|
||||||
from khoj.utils import constants, state
|
from khoj.utils import constants, state
|
||||||
from khoj.utils.config import SearchType
|
from khoj.utils.config import SearchType
|
||||||
|
@ -309,7 +309,7 @@ def configure_routes(app):
|
||||||
from khoj.routers.api_agents import api_agents
|
from khoj.routers.api_agents import api_agents
|
||||||
from khoj.routers.api_chat import api_chat
|
from khoj.routers.api_chat import api_chat
|
||||||
from khoj.routers.api_config import api_config
|
from khoj.routers.api_config import api_config
|
||||||
from khoj.routers.indexer import indexer
|
from khoj.routers.api_content import api_content
|
||||||
from khoj.routers.notion import notion_router
|
from khoj.routers.notion import notion_router
|
||||||
from khoj.routers.web_client import web_client
|
from khoj.routers.web_client import web_client
|
||||||
|
|
||||||
|
@ -317,7 +317,7 @@ def configure_routes(app):
|
||||||
app.include_router(api_chat, prefix="/api/chat")
|
app.include_router(api_chat, prefix="/api/chat")
|
||||||
app.include_router(api_agents, prefix="/api/agents")
|
app.include_router(api_agents, prefix="/api/agents")
|
||||||
app.include_router(api_config, prefix="/api/configure")
|
app.include_router(api_config, prefix="/api/configure")
|
||||||
app.include_router(indexer, prefix="/api/v1/index")
|
app.include_router(api_content, prefix="/api/content")
|
||||||
app.include_router(notion_router, prefix="/api/notion")
|
app.include_router(notion_router, prefix="/api/notion")
|
||||||
app.include_router(web_client)
|
app.include_router(web_client)
|
||||||
|
|
||||||
|
|
|
@ -998,8 +998,8 @@ To get started, just start typing below. You can also type / to see a list of co
|
||||||
// Wait for all files to be read before making the fetch request
|
// Wait for all files to be read before making the fetch request
|
||||||
Promise.all(fileReadPromises)
|
Promise.all(fileReadPromises)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
return fetch("/api/v1/index/update?force=false&client=web", {
|
return fetch("/api/content?client=web", {
|
||||||
method: "POST",
|
method: "PATCH",
|
||||||
body: formData,
|
body: formData,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
|
|
@ -19,7 +19,7 @@ from khoj.utils.yaml import save_config_to_file_updated_state
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
indexer = APIRouter()
|
api_content = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
class File(BaseModel):
|
class File(BaseModel):
|
||||||
|
@ -40,12 +40,11 @@ class IndexerInput(BaseModel):
|
||||||
docx: Optional[dict[str, bytes]] = None
|
docx: Optional[dict[str, bytes]] = None
|
||||||
|
|
||||||
|
|
||||||
@indexer.post("/update")
|
@api_content.put("")
|
||||||
@requires(["authenticated"])
|
@requires(["authenticated"])
|
||||||
async def update(
|
async def put_content(
|
||||||
request: Request,
|
request: Request,
|
||||||
files: list[UploadFile],
|
files: list[UploadFile],
|
||||||
force: bool = False,
|
|
||||||
t: Optional[Union[state.SearchType, str]] = state.SearchType.All,
|
t: Optional[Union[state.SearchType, str]] = state.SearchType.All,
|
||||||
client: Optional[str] = None,
|
client: Optional[str] = None,
|
||||||
user_agent: Optional[str] = Header(None),
|
user_agent: Optional[str] = Header(None),
|
||||||
|
@ -59,8 +58,44 @@ async def update(
|
||||||
subscribed_total_entries_size_limit=100,
|
subscribed_total_entries_size_limit=100,
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
):
|
||||||
|
return await indexer(request, files, t, True, client, user_agent, referer, host)
|
||||||
|
|
||||||
|
|
||||||
|
@api_content.patch("")
|
||||||
|
@requires(["authenticated"])
|
||||||
|
async def patch_content(
|
||||||
|
request: Request,
|
||||||
|
files: list[UploadFile],
|
||||||
|
t: Optional[Union[state.SearchType, str]] = state.SearchType.All,
|
||||||
|
client: Optional[str] = None,
|
||||||
|
user_agent: Optional[str] = Header(None),
|
||||||
|
referer: Optional[str] = Header(None),
|
||||||
|
host: Optional[str] = Header(None),
|
||||||
|
indexed_data_limiter: ApiIndexedDataLimiter = Depends(
|
||||||
|
ApiIndexedDataLimiter(
|
||||||
|
incoming_entries_size_limit=10,
|
||||||
|
subscribed_incoming_entries_size_limit=25,
|
||||||
|
total_entries_size_limit=10,
|
||||||
|
subscribed_total_entries_size_limit=100,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
):
|
||||||
|
return await indexer(request, files, t, False, client, user_agent, referer, host)
|
||||||
|
|
||||||
|
|
||||||
|
async def indexer(
|
||||||
|
request: Request,
|
||||||
|
files: list[UploadFile],
|
||||||
|
t: Optional[Union[state.SearchType, str]] = state.SearchType.All,
|
||||||
|
regenerate: bool = False,
|
||||||
|
client: Optional[str] = None,
|
||||||
|
user_agent: Optional[str] = Header(None),
|
||||||
|
referer: Optional[str] = Header(None),
|
||||||
|
host: Optional[str] = Header(None),
|
||||||
):
|
):
|
||||||
user = request.user.object
|
user = request.user.object
|
||||||
|
method = "regenerate" if regenerate else "sync"
|
||||||
index_files: Dict[str, Dict[str, str]] = {
|
index_files: Dict[str, Dict[str, str]] = {
|
||||||
"org": {},
|
"org": {},
|
||||||
"markdown": {},
|
"markdown": {},
|
||||||
|
@ -116,18 +151,17 @@ async def update(
|
||||||
None,
|
None,
|
||||||
configure_content,
|
configure_content,
|
||||||
indexer_input.model_dump(),
|
indexer_input.model_dump(),
|
||||||
force,
|
regenerate,
|
||||||
t,
|
t,
|
||||||
False,
|
|
||||||
user,
|
user,
|
||||||
)
|
)
|
||||||
if not success:
|
if not success:
|
||||||
raise RuntimeError("Failed to update content index")
|
raise RuntimeError(f"Failed to {method} {t} data sent by {client} client into content index")
|
||||||
logger.info(f"Finished processing batch indexing request")
|
logger.info(f"Finished {method} {t} data sent by {client} client into content index")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to process batch indexing request: {e}", exc_info=True)
|
logger.error(f"Failed to {method} {t} data sent by {client} client into content index: {e}", exc_info=True)
|
||||||
logger.error(
|
logger.error(
|
||||||
f'🚨 Failed to {"force " if force else ""}update {t} content index triggered via API call by {client} client: {e}',
|
f"🚨 Failed to {method} {t} data sent by {client} client into content index: {e}",
|
||||||
exc_info=True,
|
exc_info=True,
|
||||||
)
|
)
|
||||||
return Response(content="Failed", status_code=500)
|
return Response(content="Failed", status_code=500)
|
|
@ -25,7 +25,7 @@ from khoj.database.models import (
|
||||||
from khoj.processor.content.org_mode.org_to_entries import OrgToEntries
|
from khoj.processor.content.org_mode.org_to_entries import OrgToEntries
|
||||||
from khoj.processor.content.plaintext.plaintext_to_entries import PlaintextToEntries
|
from khoj.processor.content.plaintext.plaintext_to_entries import PlaintextToEntries
|
||||||
from khoj.processor.embeddings import CrossEncoderModel, EmbeddingsModel
|
from khoj.processor.embeddings import CrossEncoderModel, EmbeddingsModel
|
||||||
from khoj.routers.indexer import configure_content
|
from khoj.routers.api_content import configure_content
|
||||||
from khoj.search_type import text_search
|
from khoj.search_type import text_search
|
||||||
from khoj.utils import fs_syncer, state
|
from khoj.utils import fs_syncer, state
|
||||||
from khoj.utils.config import SearchModels
|
from khoj.utils.config import SearchModels
|
||||||
|
|
|
@ -75,7 +75,7 @@ def test_index_update_with_no_auth_key(client):
|
||||||
files = get_sample_files_data()
|
files = get_sample_files_data()
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
response = client.post("/api/v1/index/update", files=files)
|
response = client.patch("/api/content", files=files)
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
assert response.status_code == 403
|
assert response.status_code == 403
|
||||||
|
@ -89,7 +89,7 @@ def test_index_update_with_invalid_auth_key(client):
|
||||||
headers = {"Authorization": "Bearer kk-invalid-token"}
|
headers = {"Authorization": "Bearer kk-invalid-token"}
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
response = client.post("/api/v1/index/update", files=files, headers=headers)
|
response = client.patch("/api/content", files=files, headers=headers)
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
assert response.status_code == 403
|
assert response.status_code == 403
|
||||||
|
@ -130,7 +130,7 @@ def test_index_update_big_files(client):
|
||||||
headers = {"Authorization": "Bearer kk-secret"}
|
headers = {"Authorization": "Bearer kk-secret"}
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
response = client.post("/api/v1/index/update", files=files, headers=headers)
|
response = client.patch("/api/content", files=files, headers=headers)
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
assert response.status_code == 429
|
assert response.status_code == 429
|
||||||
|
@ -146,7 +146,7 @@ def test_index_update_medium_file_unsubscribed(client, api_user4: KhojApiUser):
|
||||||
headers = {"Authorization": f"Bearer {api_token}"}
|
headers = {"Authorization": f"Bearer {api_token}"}
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
response = client.post("/api/v1/index/update", files=files, headers=headers)
|
response = client.patch("/api/content", files=files, headers=headers)
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
assert response.status_code == 429
|
assert response.status_code == 429
|
||||||
|
@ -162,7 +162,7 @@ def test_index_update_normal_file_unsubscribed(client, api_user4: KhojApiUser):
|
||||||
headers = {"Authorization": f"Bearer {api_token}"}
|
headers = {"Authorization": f"Bearer {api_token}"}
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
response = client.post("/api/v1/index/update", files=files, headers=headers)
|
response = client.patch("/api/content", files=files, headers=headers)
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
@ -177,7 +177,7 @@ def test_index_update_big_files_no_billing(client):
|
||||||
headers = {"Authorization": "Bearer kk-secret"}
|
headers = {"Authorization": "Bearer kk-secret"}
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
response = client.post("/api/v1/index/update", files=files, headers=headers)
|
response = client.patch("/api/content", files=files, headers=headers)
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
@ -191,7 +191,7 @@ def test_index_update(client):
|
||||||
headers = {"Authorization": "Bearer kk-secret"}
|
headers = {"Authorization": "Bearer kk-secret"}
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
response = client.post("/api/v1/index/update", files=files, headers=headers)
|
response = client.patch("/api/content", files=files, headers=headers)
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
@ -208,8 +208,8 @@ def test_index_update_fails_if_more_than_1000_files(client, api_user4: KhojApiUs
|
||||||
headers = {"Authorization": f"Bearer {api_token}"}
|
headers = {"Authorization": f"Bearer {api_token}"}
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
ok_response = client.post("/api/v1/index/update", files=files[:1000], headers=headers)
|
ok_response = client.patch("/api/content", files=files[:1000], headers=headers)
|
||||||
bad_response = client.post("/api/v1/index/update", files=files, headers=headers)
|
bad_response = client.patch("/api/content", files=files, headers=headers)
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
assert ok_response.status_code == 200
|
assert ok_response.status_code == 200
|
||||||
|
@ -226,7 +226,7 @@ def test_regenerate_with_valid_content_type(client):
|
||||||
headers = {"Authorization": "Bearer kk-secret"}
|
headers = {"Authorization": "Bearer kk-secret"}
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
response = client.post(f"/api/v1/index/update?t={content_type}", files=files, headers=headers)
|
response = client.patch(f"/api/content?t={content_type}", files=files, headers=headers)
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
assert response.status_code == 200, f"Returned status: {response.status_code} for content type: {content_type}"
|
assert response.status_code == 200, f"Returned status: {response.status_code} for content type: {content_type}"
|
||||||
|
@ -243,7 +243,7 @@ def test_regenerate_with_github_fails_without_pat(client):
|
||||||
files = get_sample_files_data()
|
files = get_sample_files_data()
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
response = client.post(f"/api/v1/index/update?t=github", files=files, headers=headers)
|
response = client.patch(f"/api/content?t=github", files=files, headers=headers)
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
assert response.status_code == 200, f"Returned status: {response.status_code} for content type: github"
|
assert response.status_code == 200, f"Returned status: {response.status_code} for content type: github"
|
||||||
|
|
|
@ -29,7 +29,7 @@ def test_index_update_with_user2(client, api_user2: KhojApiUser):
|
||||||
source_file_symbol = set([f[1][0] for f in files])
|
source_file_symbol = set([f[1][0] for f in files])
|
||||||
|
|
||||||
headers = {"Authorization": f"Bearer {api_user2.token}"}
|
headers = {"Authorization": f"Bearer {api_user2.token}"}
|
||||||
update_response = client.post("/api/v1/index/update", files=files, headers=headers)
|
update_response = client.patch("/api/content", files=files, headers=headers)
|
||||||
search_response = client.get("/api/search?q=hardware&t=all", headers=headers)
|
search_response = client.get("/api/search?q=hardware&t=all", headers=headers)
|
||||||
results = search_response.json()
|
results = search_response.json()
|
||||||
|
|
||||||
|
@ -47,7 +47,7 @@ def test_index_update_with_user2_inaccessible_user1(client, api_user2: KhojApiUs
|
||||||
source_file_symbol = set([f[1][0] for f in files])
|
source_file_symbol = set([f[1][0] for f in files])
|
||||||
|
|
||||||
headers = {"Authorization": f"Bearer {api_user2.token}"}
|
headers = {"Authorization": f"Bearer {api_user2.token}"}
|
||||||
update_response = client.post("/api/v1/index/update", files=files, headers=headers)
|
update_response = client.patch("/api/content", files=files, headers=headers)
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
headers = {"Authorization": f"Bearer {api_user.token}"}
|
headers = {"Authorization": f"Bearer {api_user.token}"}
|
||||||
|
|
Loading…
Reference in a new issue