Skip to content

Commit 4d46b0b

Browse files
committed
✨ Now user can name the knowledge base whatever he wants without obeying the naming rule of elasticsearch
1 parent b3e45e9 commit 4d46b0b

32 files changed

+3831
-379
lines changed

backend/agents/create_agent_info.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
get_vector_db_core,
1616
get_embedding_model,
1717
)
18-
from services.tenant_config_service import get_selected_knowledge_list
18+
from services.tenant_config_service import get_selected_knowledge_list, build_knowledge_name_mapping
1919
from services.remote_mcp_service import get_remote_mcp_server_list
2020
from services.memory_config_service import build_memory_context
2121
from services.image_service import get_vlm_model
@@ -241,6 +241,7 @@ async def create_tool_config_list(agent_id, tenant_id, user_id):
241241
"index_names": index_names,
242242
"vdb_core": get_vector_db_core(),
243243
"embedding_model": get_embedding_model(tenant_id=tenant_id),
244+
"name_resolver": build_knowledge_name_mapping(tenant_id=tenant_id, user_id=user_id),
244245
}
245246
elif tool_config.class_name == "AnalyzeTextFileTool":
246247
tool_config.metadata = {

backend/apps/vectordatabase_app.py

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from fastapi import APIRouter, Body, Depends, Header, HTTPException, Path, Query
77
from fastapi.responses import JSONResponse
8+
import re
89

910
from consts.model import ChunkCreateRequest, ChunkUpdateRequest, HybridSearchRequest, IndexingResponse
1011
from nexent.vector_database.base import VectorDatabaseCore
@@ -124,8 +125,11 @@ def create_index_documents(
124125
except Exception as e:
125126
error_msg = str(e)
126127
logger.error(f"Error indexing documents: {error_msg}")
128+
127129
raise HTTPException(
128-
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=f"Error indexing documents: {error_msg}")
130+
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
131+
detail=f"Error indexing documents: {error_msg}"
132+
)
129133

130134

131135
@router.get("/{index_name}/files")
@@ -229,15 +233,18 @@ async def get_document_error_info(
229233
error_code = None
230234

231235
if raw_error:
232-
text = raw_error
233-
234236
# Try to parse JSON (new format with error_code only)
235-
if isinstance(text, str) and text.strip().startswith("{"):
237+
try:
238+
parsed = json.loads(raw_error)
239+
if isinstance(parsed, dict) and "error_code" in parsed:
240+
error_code = parsed.get("error_code")
241+
except Exception:
242+
# Fallback: regex extraction if JSON parsing fails
236243
try:
237-
parsed = json.loads(text)
238-
if isinstance(parsed, dict):
239-
if "error_code" in parsed:
240-
error_code = parsed.get("error_code")
244+
match = re.search(
245+
r'["\']error_code["\']\s*:\s*["\']([^"\']+)["\']', raw_error)
246+
if match:
247+
error_code = match.group(1)
241248
except Exception:
242249
pass
243250

backend/data_process/tasks.py

Lines changed: 76 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from typing import Any, Dict, Optional
1111

1212
import aiohttp
13+
import re
1314
import ray
1415
from celery import Task, chain, states
1516
from celery.exceptions import Retry
@@ -41,11 +42,33 @@ def extract_error_code(reason: str, parsed_error: Optional[Dict] = None) -> Opti
4142
Extract error code from error message or parsed error dict.
4243
Returns error code if matched, None otherwise.
4344
"""
44-
# First check if error_code is already in parsed_error
45+
# 1) parsed_error dict
4546
if parsed_error and isinstance(parsed_error, dict):
46-
error_code = parsed_error.get("error_code")
47-
if error_code:
48-
return error_code
47+
code = parsed_error.get("error_code")
48+
if code:
49+
return code
50+
51+
# 2) try parse reason as JSON
52+
try:
53+
parsed = json.loads(reason)
54+
if isinstance(parsed, dict):
55+
code = parsed.get("error_code")
56+
if code:
57+
return code
58+
detail = parsed.get("detail")
59+
if isinstance(detail, dict) and detail.get("error_code"):
60+
return detail.get("error_code")
61+
except Exception:
62+
pass
63+
64+
# 3) regex from raw string (supports single/double quotes)
65+
try:
66+
match = re.search(
67+
r'["\']error_code["\']\s*:\s*["\']([^"\']+)["\']', reason)
68+
if match:
69+
return match.group(1)
70+
except Exception:
71+
pass
4972

5073
return "unknown_error"
5174

@@ -688,68 +711,61 @@ async def index_documents():
688711

689712
try:
690713
connector = aiohttp.TCPConnector(verify_ssl=False)
691-
# Increased timeout for large documents and slow ES bulk operations
692-
# Use generous total timeout to avoid marking long-running but successful
693-
# indexing as failed.
694714
timeout = aiohttp.ClientTimeout(total=600)
695715

696716
async with aiohttp.ClientSession(connector=connector, timeout=timeout) as session:
697717
async with session.post(
698718
full_url,
699719
headers=headers,
700720
json=formatted_chunks,
701-
raise_for_status=True
721+
raise_for_status=False
702722
) as response:
703-
result = await response.json()
723+
text = await response.text()
724+
status = response.status
725+
# Try parse JSON body for structured error_code/message
726+
parsed_body = None
727+
try:
728+
parsed_body = json.loads(text)
729+
except Exception:
730+
parsed_body = None
731+
732+
if status >= 400:
733+
error_code = None
734+
if isinstance(parsed_body, dict):
735+
error_code = parsed_body.get("error_code")
736+
detail = parsed_body.get("detail")
737+
if isinstance(detail, dict) and detail.get("error_code"):
738+
error_code = detail.get("error_code")
739+
elif isinstance(detail, str):
740+
try:
741+
parsed_detail = json.loads(detail)
742+
if isinstance(parsed_detail, dict):
743+
error_code = parsed_detail.get(
744+
"error_code", error_code)
745+
except Exception:
746+
pass
747+
748+
if not error_code:
749+
try:
750+
match = re.search(
751+
r'["\']error_code["\']\s*:\s*["\']([^"\']+)["\']', text)
752+
if match:
753+
error_code = match.group(1)
754+
except Exception:
755+
pass
756+
757+
if error_code:
758+
# Raise flat payload to avoid nested JSON and preserve error_code
759+
raise Exception(json.dumps({
760+
"error_code": error_code
761+
}, ensure_ascii=False))
762+
763+
raise Exception(
764+
f"ElasticSearch service returned HTTP {status}")
765+
766+
result = parsed_body if isinstance(parsed_body, dict) else await response.json()
704767
return result
705768

706-
except aiohttp.ClientResponseError as e:
707-
# 400: embedding model reports chunk count exceeds concurrency
708-
if e.status == 400:
709-
raise Exception(json.dumps({
710-
"message": f"ElasticSearch service returned 400 Bad Request: {str(e)}",
711-
"index_name": original_index_name,
712-
"task_name": "forward",
713-
"source": original_source,
714-
"original_filename": original_filename,
715-
"error_code": "embedding_chunks_exceed_limit"
716-
}, ensure_ascii=False))
717-
718-
# Timeout from Elasticsearch refresh / bulk operations: stop retrying and treat as es_bulk_failed
719-
timeout_markers = [
720-
"Connection timeout caused by",
721-
"Read timed out",
722-
"ReadTimeoutError"
723-
]
724-
if any(marker in str(e) for marker in timeout_markers):
725-
raise Exception(json.dumps({
726-
"message": f"ElasticSearch operation timed out: {str(e)}",
727-
"index_name": original_index_name,
728-
"task_name": "forward",
729-
"source": original_source,
730-
"original_filename": original_filename,
731-
"error_code": "es_bulk_failed"
732-
}, ensure_ascii=False))
733-
734-
# 503: vector service busy: bubble up immediately, let caller decide
735-
if e.status == 503:
736-
raise Exception(json.dumps({
737-
"message": f"ElasticSearch service unavailable: {str(e)}",
738-
"index_name": original_index_name,
739-
"task_name": "forward",
740-
"source": original_source,
741-
"original_filename": original_filename,
742-
"error_code": "vector_service_busy"
743-
}, ensure_ascii=False))
744-
745-
# Other client response errors: bubble up
746-
raise Exception(json.dumps({
747-
"message": f"ElasticSearch service unavailable: {str(e)}",
748-
"index_name": original_index_name,
749-
"task_name": "forward",
750-
"source": original_source,
751-
"original_filename": original_filename
752-
}, ensure_ascii=False))
753769
except aiohttp.ClientConnectorError as e:
754770
logger.error(
755771
f"[{self.request.id}] FORWARD TASK: Connection error to {full_url}: {str(e)}")
@@ -879,6 +895,10 @@ async def index_documents():
879895
}
880896
except Exception as e:
881897
# If it's an Exception, all go here (including our custom JSON message)
898+
# Important: if this is a Celery Retry, re-raise immediately without recording error_code
899+
if isinstance(e, Retry):
900+
raise
901+
882902
task_id = self.request.id
883903
try:
884904
error_info = json.loads(str(e))

backend/database/knowledge_db.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,7 @@ def get_knowledge_info_by_knowledge_ids(knowledge_ids: List[str]) -> List[Dict[s
197197
knowledge_info.append({
198198
"knowledge_id": item.knowledge_id,
199199
"index_name": item.index_name,
200+
"knowledge_name": item.knowledge_name,
200201
"knowledge_sources": item.knowledge_sources,
201202
"embedding_model_name": item.embedding_model_name
202203
})

backend/services/model_management_service.py

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -241,37 +241,11 @@ async def update_single_model_for_tenant(
241241
m.get("model_type") == "multi_embedding" for m in existing_models
242242
)
243243

244-
async def _try_update_embedding_dimension(model: Dict[str, Any], update_payload: Dict[str, Any]):
245-
"""Run embedding dimension check when updating embedding models so stored max_tokens stays accurate."""
246-
model_type = model.get("model_type")
247-
if model_type not in ("embedding", "multi_embedding"):
248-
return
249-
250-
base_url = update_payload.get(
251-
"base_url", model.get("base_url", ""))
252-
api_key = update_payload.get("api_key", model.get("api_key", ""))
253-
254-
if not base_url or not api_key:
255-
return
256-
257-
combined_config = {
258-
"model_type": model_type,
259-
"model_repo": model.get("model_repo", ""),
260-
"model_name": add_repo_to_name(model.get("model_repo", ""), model.get("model_name", "")),
261-
"base_url": base_url,
262-
"api_key": api_key,
263-
}
264-
265-
dimension = await embedding_dimension_check(combined_config)
266-
if dimension:
267-
update_payload["max_tokens"] = dimension
268-
269244
if has_multi_embedding:
270245
# Update both embedding and multi_embedding records
271246
for model in existing_models:
272247
# Prepare update data, excluding model_type to preserve original type
273248
update_data = {k: v for k, v in model_data.items() if k not in ["model_id", "model_type"]}
274-
await _try_update_embedding_dimension(model, update_data)
275249
update_model_record(model["model_id"], update_data, user_id)
276250
logging.debug(
277251
f"Model {current_display_name} (embedding + multi_embedding) updated successfully")
@@ -280,7 +254,6 @@ async def _try_update_embedding_dimension(model: Dict[str, Any], update_payload:
280254
current_model = existing_models[0]
281255
current_model_id = current_model["model_id"]
282256
update_data = {k: v for k, v in model_data.items() if k != "model_id"}
283-
await _try_update_embedding_dimension(current_model, update_data)
284257
update_model_record(current_model_id, update_data, user_id)
285258
logging.debug(f"Model {current_display_name} updated successfully")
286259
except LookupError:

backend/services/model_provider_service.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -187,12 +187,19 @@ async def prepare_model_dict(provider: str, model: dict, model_url: str, model_a
187187

188188
# Build the canonical representation using the existing Pydantic schema for
189189
# consistency of validation and default handling.
190+
# For embedding/multi_embedding models, max_tokens will be set via connectivity check later,
191+
# so use 0 as placeholder if not provided
192+
model_type = model["model_type"]
193+
is_embedding_type = model_type in ["embedding", "multi_embedding"]
194+
max_tokens_value = model.get(
195+
"max_tokens", 0) if not is_embedding_type else 0
196+
190197
model_obj = ModelRequest(
191198
model_factory=provider,
192199
model_name=model_name,
193-
model_type=model["model_type"],
200+
model_type=model_type,
194201
api_key=model_api_key,
195-
max_tokens=model["max_tokens"],
202+
max_tokens=max_tokens_value,
196203
display_name=model_display_name,
197204
expected_chunk_size=expected_chunk_size,
198205
maximum_chunk_size=maximum_chunk_size,

backend/services/redis_service.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,12 @@ def client(self) -> redis.Redis:
2222
if self._client is None:
2323
if not REDIS_URL:
2424
raise ValueError("REDIS_URL environment variable is not set")
25-
self._client = redis.from_url(REDIS_URL, socket_timeout=5, socket_connect_timeout=5)
25+
self._client = redis.from_url(
26+
REDIS_URL,
27+
socket_timeout=5,
28+
socket_connect_timeout=5,
29+
decode_responses=True
30+
)
2631
return self._client
2732

2833
@property
@@ -673,8 +678,6 @@ def save_error_info(self, task_id: str, error_reason: str, ttl_days: int = 30) -
673678
# Verify the save by reading it back
674679
verify = self.client.get(reason_key)
675680
if verify:
676-
if isinstance(verify, bytes):
677-
verify = verify.decode('utf-8')
678681
logger.debug(f"Verified error info saved for task {task_id}: {verify[:100]}...")
679682
else:
680683
logger.warning(f"Failed to verify error info save for task {task_id}")
@@ -760,11 +763,8 @@ def get_error_info(self, task_id: str) -> Optional[str]:
760763
try:
761764
reason_key = f"error:reason:{task_id}"
762765
reason = self.client.get(reason_key)
763-
if reason:
764-
if isinstance(reason, bytes):
765-
return reason.decode('utf-8')
766-
return reason
767-
return None
766+
# With decode_responses=True, reason is already a string
767+
return reason if reason else None
768768
except Exception as e:
769769
logger.error(
770770
f"Failed to get error info for task {task_id}: {str(e)}")

backend/services/tenant_config_service.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,19 @@ def delete_selected_knowledge_by_index_name(tenant_id: str, user_id: str, index_
6666
return False
6767

6868
return True
69+
70+
71+
def build_knowledge_name_mapping(tenant_id: str, user_id: str):
72+
"""
73+
Build mapping from user-facing knowledge_name to internal index_name for the selected knowledge bases.
74+
Falls back to using index_name as key when knowledge_name is missing for backward compatibility.
75+
"""
76+
knowledge_info_list = get_selected_knowledge_list(
77+
tenant_id=tenant_id, user_id=user_id)
78+
mapping = {}
79+
for info in knowledge_info_list:
80+
key = info.get("knowledge_name") or info.get("index_name")
81+
value = info.get("index_name")
82+
if key and value:
83+
mapping[key] = value
84+
return mapping

0 commit comments

Comments
 (0)