2024-08-15 09:17:36 +08:00
#
# Copyright 2024 The InfiniFlow Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
2025-11-04 11:51:12 +08:00
2024-08-29 14:31:31 +08:00
import functools
2024-08-15 09:17:36 +08:00
import json
2025-04-08 16:09:03 +08:00
import logging
2025-08-25 18:29:24 +08:00
import os
2024-08-15 09:17:36 +08:00
import time
2025-05-09 19:17:08 +08:00
from copy import deepcopy
2024-08-15 09:17:36 +08:00
from functools import wraps
2024-08-29 14:31:31 +08:00
import requests
2025-07-23 15:08:36 +08:00
import trio
2024-08-15 09:17:36 +08:00
from flask import (
2025-04-08 16:09:03 +08:00
Response ,
jsonify ,
)
2025-09-25 16:15:15 +08:00
from flask_login import current_user
2025-04-08 16:09:03 +08:00
from flask import (
2024-08-15 09:17:36 +08:00
request as flask_request ,
)
2025-05-06 17:38:06 +08:00
from peewee import OperationalError
2024-08-15 09:17:36 +08:00
2025-11-05 08:01:39 +08:00
from common . constants import ActiveEnum
2025-04-08 16:09:03 +08:00
from api . db . db_models import APIToken
2025-11-04 11:10:29 +08:00
from api . utils . json_encode import CustomJSONEncoder
2025-07-23 15:08:36 +08:00
from rag . utils . mcp_tool_call_conn import MCPToolCallSession , close_multiple_mcp_toolcall_sessions
2025-11-04 23:47:50 -03:00
from api . db . services . tenant_llm_service import LLMFactoriesService
2025-11-04 11:51:12 +08:00
from common . connection_utils import timeout
2025-11-04 19:25:25 +08:00
from common . constants import RetCode
2025-11-06 09:36:38 +08:00
from common import settings
2024-11-15 17:30:56 +08:00
2025-04-08 16:09:03 +08:00
requests . models . complexjson . dumps = functools . partial ( json . dumps , cls = CustomJSONEncoder )
2024-08-15 09:17:36 +08:00
2025-10-10 09:17:36 +08:00
2025-09-02 13:47:34 +05:30
def serialize_for_json ( obj ) :
"""
Recursively serialize objects to make them JSON serializable .
Handles ModelMetaclass and other non - serializable objects .
"""
2025-11-04 23:47:50 -03:00
if hasattr ( obj , " __dict__ " ) :
2025-09-02 13:47:34 +05:30
# For objects with __dict__, try to serialize their attributes
try :
2025-11-04 23:47:50 -03:00
return { key : serialize_for_json ( value ) for key , value in obj . __dict__ . items ( ) if not key . startswith ( " _ " ) }
2025-09-02 13:47:34 +05:30
except ( AttributeError , TypeError ) :
return str ( obj )
2025-11-04 23:47:50 -03:00
elif hasattr ( obj , " __name__ " ) :
2025-09-02 13:47:34 +05:30
# For classes and metaclasses, return their name
2025-11-04 23:47:50 -03:00
return f " < { obj . __module__ } . { obj . __name__ } > " if hasattr ( obj , " __module__ " ) else f " < { obj . __name__ } > "
2025-09-02 13:47:34 +05:30
elif isinstance ( obj , ( list , tuple ) ) :
return [ serialize_for_json ( item ) for item in obj ]
elif isinstance ( obj , dict ) :
return { key : serialize_for_json ( value ) for key , value in obj . items ( ) }
elif isinstance ( obj , ( str , int , float , bool ) ) or obj is None :
return obj
else :
# Fallback: convert to string representation
return str ( obj )
2024-08-15 09:17:36 +08:00
2025-11-04 23:47:50 -03:00
2025-11-04 15:12:53 +08:00
def get_data_error_result ( code = RetCode . DATA_ERROR , message = " Sorry! Data missing! " ) :
2024-12-31 14:31:31 +08:00
logging . exception ( Exception ( message ) )
2025-04-08 16:09:03 +08:00
result_dict = { " code " : code , " message " : message }
2024-08-15 09:17:36 +08:00
response = { }
for key , value in result_dict . items ( ) :
2024-11-05 11:02:31 +08:00
if value is None and key != " code " :
2024-08-15 09:17:36 +08:00
continue
else :
response [ key ] = value
return jsonify ( response )
def server_error_response ( e ) :
2024-11-14 17:13:48 +08:00
logging . exception ( e )
2024-08-15 09:17:36 +08:00
try :
2025-10-14 14:13:10 +08:00
msg = repr ( e ) . lower ( )
if getattr ( e , " code " , None ) == 401 or ( " unauthorized " in msg ) or ( " 401 " in msg ) :
2025-11-04 15:12:53 +08:00
return get_json_result ( code = RetCode . UNAUTHORIZED , message = repr ( e ) )
2025-10-14 14:13:10 +08:00
except Exception as ex :
logging . warning ( f " error checking authorization: { ex } " )
2024-08-15 09:17:36 +08:00
if len ( e . args ) > 1 :
2025-09-02 13:47:34 +05:30
try :
serialized_data = serialize_for_json ( e . args [ 1 ] )
2025-11-04 15:12:53 +08:00
return get_json_result ( code = RetCode . EXCEPTION_ERROR , message = repr ( e . args [ 0 ] ) , data = serialized_data )
2025-09-02 13:47:34 +05:30
except Exception :
2025-11-04 15:12:53 +08:00
return get_json_result ( code = RetCode . EXCEPTION_ERROR , message = repr ( e . args [ 0 ] ) , data = None )
2024-12-30 18:38:51 +08:00
if repr ( e ) . find ( " index_not_found_exception " ) > = 0 :
2025-11-04 23:47:50 -03:00
return get_json_result ( code = RetCode . EXCEPTION_ERROR , message = " No chunk found, please upload file and parse it. " )
2024-12-30 18:38:51 +08:00
2025-11-04 15:12:53 +08:00
return get_json_result ( code = RetCode . EXCEPTION_ERROR , message = repr ( e ) )
2024-08-15 09:17:36 +08:00
def validate_request ( * args , * * kwargs ) :
def wrapper ( func ) :
@wraps ( func )
def decorated_function ( * _args , * * _kwargs ) :
input_arguments = flask_request . json or flask_request . form . to_dict ( )
no_arguments = [ ]
error_arguments = [ ]
for arg in args :
if arg not in input_arguments :
no_arguments . append ( arg )
for k , v in kwargs . items ( ) :
config_value = input_arguments . get ( k , None )
if config_value is None :
no_arguments . append ( k )
elif isinstance ( v , ( tuple , list ) ) :
if config_value not in v :
error_arguments . append ( ( k , set ( v ) ) )
elif config_value != v :
error_arguments . append ( ( k , v ) )
if no_arguments or error_arguments :
error_string = " "
if no_arguments :
2025-04-08 16:09:03 +08:00
error_string + = " required argument are missing: {} ; " . format ( " , " . join ( no_arguments ) )
2024-08-15 09:17:36 +08:00
if error_arguments :
2025-11-04 23:47:50 -03:00
error_string + = " required argument values: {} " . format ( " , " . join ( [ " {} = {} " . format ( a [ 0 ] , a [ 1 ] ) for a in error_arguments ] ) )
2025-11-04 15:12:53 +08:00
return get_json_result ( code = RetCode . ARGUMENT_ERROR , message = error_string )
2024-08-15 09:17:36 +08:00
return func ( * _args , * * _kwargs )
2024-08-29 14:31:31 +08:00
2024-08-15 09:17:36 +08:00
return decorated_function
2024-08-29 14:31:31 +08:00
2024-08-15 09:17:36 +08:00
return wrapper
2025-01-09 17:07:21 +08:00
2024-12-02 17:15:19 +08:00
def not_allowed_parameters ( * params ) :
def decorator ( f ) :
def wrapper ( * args , * * kwargs ) :
input_arguments = flask_request . json or flask_request . form . to_dict ( )
for param in params :
if param in input_arguments :
2025-11-04 23:47:50 -03:00
return get_json_result ( code = RetCode . ARGUMENT_ERROR , message = f " Parameter { param } isn ' t allowed " )
2024-12-02 17:15:19 +08:00
return f ( * args , * * kwargs )
2025-01-09 17:07:21 +08:00
2024-12-02 17:15:19 +08:00
return wrapper
2025-01-09 17:07:21 +08:00
2024-12-02 17:15:19 +08:00
return decorator
2024-08-15 09:17:36 +08:00
2025-09-25 16:15:15 +08:00
def active_required ( f ) :
@wraps ( f )
def wrapper ( * args , * * kwargs ) :
2025-10-10 17:07:55 +08:00
from api . db . services import UserService
2025-11-04 23:47:50 -03:00
2025-09-25 16:15:15 +08:00
user_id = current_user . id
usr = UserService . filter_by_id ( user_id )
# check is_active
if not usr or not usr . is_active == ActiveEnum . ACTIVE . value :
2025-11-04 15:12:53 +08:00
return get_json_result ( code = RetCode . FORBIDDEN , message = " User isn ' t active, please activate first. " )
2025-09-25 16:15:15 +08:00
return f ( * args , * * kwargs )
2025-10-10 09:17:36 +08:00
2025-09-25 16:15:15 +08:00
return wrapper
2025-11-04 15:12:53 +08:00
def get_json_result ( code : RetCode = RetCode . SUCCESS , message = " success " , data = None ) :
2024-11-05 11:02:31 +08:00
response = { " code " : code , " message " : message , " data " : data }
2024-08-15 09:17:36 +08:00
return jsonify ( response )
2025-01-09 17:07:21 +08:00
2024-10-15 17:47:24 +08:00
def apikey_required ( func ) :
@wraps ( func )
def decorated_function ( * args , * * kwargs ) :
2025-04-08 16:09:03 +08:00
token = flask_request . headers . get ( " Authorization " ) . split ( ) [ 1 ]
2024-10-15 17:47:24 +08:00
objs = APIToken . query ( token = token )
if not objs :
2025-11-04 15:12:53 +08:00
return build_error_result ( message = " API-KEY is invalid! " , code = RetCode . FORBIDDEN )
2025-04-08 16:09:03 +08:00
kwargs [ " tenant_id " ] = objs [ 0 ] . tenant_id
2024-10-15 17:47:24 +08:00
return func ( * args , * * kwargs )
return decorated_function
2025-11-04 15:12:53 +08:00
def build_error_result ( code = RetCode . FORBIDDEN , message = " success " ) :
2024-11-05 11:02:31 +08:00
response = { " code " : code , " message " : message }
2024-10-15 17:47:24 +08:00
response = jsonify ( response )
2024-11-05 11:02:31 +08:00
response . status_code = code
2024-10-15 17:47:24 +08:00
return response
2024-08-15 09:17:36 +08:00
2025-11-04 15:12:53 +08:00
def construct_json_result ( code : RetCode = RetCode . SUCCESS , message = " success " , data = None ) :
2024-08-15 09:17:36 +08:00
if data is None :
return jsonify ( { " code " : code , " message " : message } )
else :
return jsonify ( { " code " : code , " message " : message , " data " : data } )
2025-11-04 23:47:50 -03:00
2024-08-29 14:31:31 +08:00
def token_required ( func ) :
@wraps ( func )
def decorated_function ( * args , * * kwargs ) :
2025-09-09 10:52:18 +08:00
if os . environ . get ( " DISABLE_SDK " ) :
return get_json_result ( data = False , message = " `Authorization` can ' t be empty " )
2025-04-08 16:09:03 +08:00
authorization_str = flask_request . headers . get ( " Authorization " )
2024-12-20 17:34:16 +08:00
if not authorization_str :
2025-01-09 17:07:21 +08:00
return get_json_result ( data = False , message = " `Authorization` can ' t be empty " )
authorization_list = authorization_str . split ( )
2024-11-07 19:26:03 +08:00
if len ( authorization_list ) < 2 :
2025-01-09 17:07:21 +08:00
return get_json_result ( data = False , message = " Please check your authorization format. " )
2024-11-07 19:26:03 +08:00
token = authorization_list [ 1 ]
2024-08-29 14:31:31 +08:00
objs = APIToken . query ( token = token )
if not objs :
2025-11-04 23:47:50 -03:00
return get_json_result ( data = False , message = " Authentication error: API key is invalid! " , code = RetCode . AUTHENTICATION_ERROR )
2025-04-08 16:09:03 +08:00
kwargs [ " tenant_id " ] = objs [ 0 ] . tenant_id
2024-08-29 14:31:31 +08:00
return func ( * args , * * kwargs )
return decorated_function
2024-10-11 09:55:27 +08:00
2024-10-15 16:11:26 +08:00
2025-11-04 15:12:53 +08:00
def get_result ( code = RetCode . SUCCESS , message = " " , data = None , total = None ) :
2025-10-10 11:20:55 +08:00
"""
Standard API response format :
{
" code " : 0 ,
" data " : [ . . . ] , # List or object, backward compatible
" total " : 47 , # Optional field for pagination
" message " : " ... " # Error or status message
}
"""
response = { " code " : code }
2025-11-04 15:12:53 +08:00
if code == RetCode . SUCCESS :
2024-10-11 09:55:27 +08:00
if data is not None :
2025-10-10 11:20:55 +08:00
response [ " data " ] = data
if total is not None :
response [ " total_datasets " ] = total
2024-10-11 09:55:27 +08:00
else :
2025-10-10 11:20:55 +08:00
response [ " message " ] = message or " Error "
2024-10-11 09:55:27 +08:00
2025-10-10 11:20:55 +08:00
return jsonify ( response )
2024-10-15 16:11:26 +08:00
2025-11-04 23:47:50 -03:00
2025-04-08 16:09:03 +08:00
def get_error_data_result (
2025-11-04 23:47:50 -03:00
message = " Sorry! Data missing! " ,
code = RetCode . DATA_ERROR ,
2025-04-08 16:09:03 +08:00
) :
result_dict = { " code " : code , " message " : message }
2024-10-11 09:55:27 +08:00
response = { }
for key , value in result_dict . items ( ) :
if value is None and key != " code " :
continue
else :
response [ key ] = value
2024-10-15 16:11:26 +08:00
return jsonify ( response )
2025-04-29 16:53:57 +08:00
def get_error_argument_result ( message = " Invalid arguments " ) :
2025-11-04 15:12:53 +08:00
return get_result ( code = RetCode . ARGUMENT_ERROR , message = message )
2025-04-29 16:53:57 +08:00
2025-05-20 09:58:26 +08:00
def get_error_permission_result ( message = " Permission error " ) :
2025-11-04 15:12:53 +08:00
return get_result ( code = RetCode . PERMISSION_ERROR , message = message )
2025-05-20 09:58:26 +08:00
def get_error_operating_result ( message = " Operating error " ) :
2025-11-04 15:12:53 +08:00
return get_result ( code = RetCode . OPERATING_ERROR , message = message )
2025-05-20 09:58:26 +08:00
2025-10-30 09:31:36 +08:00
def generate_confirmation_token ( ) :
import secrets
2025-11-04 23:47:50 -03:00
2025-10-30 09:31:36 +08:00
return " ragflow- " + secrets . token_urlsafe ( 32 )
2024-10-21 14:29:06 +08:00
2025-01-09 17:07:21 +08:00
def get_parser_config ( chunk_method , parser_config ) :
2024-10-23 12:02:18 +08:00
if not chunk_method :
chunk_method = " naive "
2025-07-23 09:29:37 +08:00
2025-08-26 19:35:29 +08:00
# Define default configurations for each chunking method
2025-01-09 17:07:21 +08:00
key_mapping = {
2025-11-04 13:45:14 +08:00
" naive " : {
" layout_recognize " : " DeepDOC " ,
" chunk_token_num " : 512 ,
" delimiter " : " \n " ,
" auto_keywords " : 0 ,
" auto_questions " : 0 ,
" html4excel " : False ,
" topn_tags " : 3 ,
" raptor " : {
" use_raptor " : True ,
" prompt " : " Please summarize the following paragraphs. Be careful with the numbers, do not make things up. Paragraphs as following: \n {cluster_content} \n The above is the content you need to summarize. " ,
" max_token " : 256 ,
" threshold " : 0.1 ,
" max_cluster " : 64 ,
" random_seed " : 0 ,
} ,
" graphrag " : {
" use_graphrag " : True ,
" entity_types " : [
" organization " ,
" person " ,
" geo " ,
" event " ,
" category " ,
] ,
" method " : " light " ,
} ,
} ,
2025-07-23 09:29:37 +08:00
" qa " : { " raptor " : { " use_raptor " : False } , " graphrag " : { " use_graphrag " : False } } ,
2025-01-09 17:07:21 +08:00
" tag " : None ,
" resume " : None ,
2025-07-23 09:29:37 +08:00
" manual " : { " raptor " : { " use_raptor " : False } , " graphrag " : { " use_graphrag " : False } } ,
2025-01-09 17:07:21 +08:00
" table " : None ,
2025-07-23 09:29:37 +08:00
" paper " : { " raptor " : { " use_raptor " : False } , " graphrag " : { " use_graphrag " : False } } ,
" book " : { " raptor " : { " use_raptor " : False } , " graphrag " : { " use_graphrag " : False } } ,
" laws " : { " raptor " : { " use_raptor " : False } , " graphrag " : { " use_graphrag " : False } } ,
" presentation " : { " raptor " : { " use_raptor " : False } , " graphrag " : { " use_graphrag " : False } } ,
2025-01-09 17:07:21 +08:00
" one " : None ,
2025-07-23 09:29:37 +08:00
" knowledge_graph " : {
" chunk_token_num " : 8192 ,
" delimiter " : r " \ n " ,
" entity_types " : [ " organization " , " person " , " location " , " event " , " time " ] ,
" raptor " : { " use_raptor " : False } ,
" graphrag " : { " use_graphrag " : False } ,
} ,
2025-01-09 17:07:21 +08:00
" email " : None ,
2025-04-08 16:09:03 +08:00
" picture " : None ,
}
2025-07-23 09:29:37 +08:00
default_config = key_mapping [ chunk_method ]
# If no parser_config provided, return default
if not parser_config :
return default_config
# If parser_config is provided, merge with defaults to ensure required fields exist
if default_config is None :
return parser_config
# Ensure raptor and graphrag fields have default values if not provided
merged_config = deep_merge ( default_config , parser_config )
return merged_config
2025-03-07 15:34:34 +08:00
2025-11-04 23:47:50 -03:00
def get_data_openai ( id = None , created = None , model = None , prompt_tokens = 0 , completion_tokens = 0 , content = None , finish_reason = None , object = " chat.completion " , param = None , stream = False ) :
2025-04-29 16:53:57 +08:00
total_tokens = prompt_tokens + completion_tokens
2025-08-05 17:47:25 +08:00
if stream :
return {
" id " : f " { id } " ,
" object " : " chat.completion.chunk " ,
" model " : model ,
2025-11-04 23:47:50 -03:00
" choices " : [
{
" delta " : { " content " : content } ,
" finish_reason " : finish_reason ,
" index " : 0 ,
}
] ,
2025-08-05 17:47:25 +08:00
}
2025-04-03 15:51:37 +07:00
return {
2025-04-29 16:53:57 +08:00
" id " : f " { id } " ,
2025-04-03 15:51:37 +07:00
" object " : object ,
" created " : int ( time . time ( ) ) if created else None ,
" model " : model ,
2025-04-29 16:53:57 +08:00
" param " : param ,
2025-04-03 15:51:37 +07:00
" usage " : {
" prompt_tokens " : prompt_tokens ,
" completion_tokens " : completion_tokens ,
" total_tokens " : total_tokens ,
2025-08-05 17:47:25 +08:00
" completion_tokens_details " : {
" reasoning_tokens " : 0 ,
" accepted_prediction_tokens " : 0 ,
" rejected_prediction_tokens " : 0 ,
} ,
2025-04-03 15:51:37 +07:00
} ,
2025-11-04 23:47:50 -03:00
" choices " : [
{
" message " : { " role " : " assistant " , " content " : content } ,
" logprobs " : None ,
" finish_reason " : finish_reason ,
" index " : 0 ,
}
] ,
2025-04-29 16:53:57 +08:00
}
2025-03-21 14:05:17 +08:00
def check_duplicate_ids ( ids , id_type = " item " ) :
"""
Check for duplicate IDs in a list and return unique IDs and error messages .
2025-04-08 16:09:03 +08:00
Args :
2025-03-21 14:05:17 +08:00
ids ( list ) : List of IDs to check for duplicates
id_type ( str ) : Type of ID for error messages ( e . g . , ' document ' , ' dataset ' , ' chunk ' )
Returns :
tuple : ( unique_ids , error_messages )
- unique_ids ( list ) : List of unique IDs
- error_messages ( list ) : List of error messages for duplicate IDs
"""
id_count = { }
duplicate_messages = [ ]
2025-04-08 16:09:03 +08:00
2025-03-21 14:05:17 +08:00
# Count occurrences of each ID
for id_value in ids :
id_count [ id_value ] = id_count . get ( id_value , 0 ) + 1
2025-04-08 16:09:03 +08:00
2025-03-21 14:05:17 +08:00
# Check for duplicates
for id_value , count in id_count . items ( ) :
if count > 1 :
duplicate_messages . append ( f " Duplicate { id_type } ids: { id_value } " )
2025-04-08 16:09:03 +08:00
2025-03-21 14:05:17 +08:00
# Return unique IDs and error messages
return list ( set ( ids ) ) , duplicate_messages
2025-05-06 17:38:06 +08:00
def verify_embedding_availability ( embd_id : str , tenant_id : str ) - > tuple [ bool , Response | None ] :
2025-10-10 17:07:55 +08:00
from api . db . services . llm_service import LLMService
from api . db . services . tenant_llm_service import TenantLLMService
2025-11-04 23:47:50 -03:00
2025-05-09 19:17:08 +08:00
"""
Verifies availability of an embedding model for a specific tenant .
2025-05-06 17:38:06 +08:00
2025-06-05 19:03:46 +08:00
Performs comprehensive verification through :
1. Identifier Parsing : Decomposes embd_id into name and factory components
2. System Verification : Checks model registration in LLMService
3. Tenant Authorization : Validates tenant - specific model assignments
4. Built - in Model Check : Confirms inclusion in predefined system models
2025-05-06 17:38:06 +08:00
Args :
embd_id ( str ) : Unique identifier for the embedding model in format " model_name@factory "
tenant_id ( str ) : Tenant identifier for access control
Returns :
tuple [ bool , Response | None ] :
- First element ( bool ) :
- True : Model is available and authorized
- False : Validation failed
- Second element contains :
- None on success
- Error detail dict on failure
Raises :
ValueError : When model identifier format is invalid
OperationalError : When database connection fails ( auto - handled )
Examples :
>> > verify_embedding_availability ( " text-embedding@openai " , " tenant_123 " )
( True , None )
>> > verify_embedding_availability ( " invalid_model " , " tenant_123 " )
( False , { ' code ' : 101 , ' message ' : " Unsupported model: <invalid_model> " } )
"""
try :
llm_name , llm_factory = TenantLLMService . split_model_name_and_factory ( embd_id )
2025-06-05 12:05:58 +08:00
in_llm_service = bool ( LLMService . query ( llm_name = llm_name , fid = llm_factory , model_type = " embedding " ) )
2025-06-05 19:03:46 +08:00
2025-05-06 17:38:06 +08:00
tenant_llms = TenantLLMService . get_my_llms ( tenant_id = tenant_id )
2025-11-04 23:47:50 -03:00
is_tenant_model = any ( llm [ " llm_name " ] == llm_name and llm [ " llm_factory " ] == llm_factory and llm [ " model_type " ] == " embedding " for llm in tenant_llms )
2025-05-06 17:38:06 +08:00
2025-11-04 23:47:50 -03:00
is_builtin_model = llm_factory == " Builtin "
2025-06-05 19:03:46 +08:00
if not ( is_builtin_model or is_tenant_model or in_llm_service ) :
2025-06-05 12:05:58 +08:00
return False , get_error_argument_result ( f " Unsupported model: < { embd_id } > " )
2025-05-06 17:38:06 +08:00
if not ( is_builtin_model or is_tenant_model ) :
return False , get_error_argument_result ( f " Unauthorized model: < { embd_id } > " )
except OperationalError as e :
logging . exception ( e )
return False , get_error_data_result ( message = " Database operation failed " )
return True , None
2025-05-09 19:17:08 +08:00
def deep_merge ( default : dict , custom : dict ) - > dict :
"""
Recursively merges two dictionaries with priority given to ` custom ` values .
Creates a deep copy of the ` default ` dictionary and iteratively merges nested
dictionaries using a stack - based approach . Non - dict values in ` custom ` will
completely override corresponding entries in ` default ` .
Args :
default ( dict ) : Base dictionary containing default values .
custom ( dict ) : Dictionary containing overriding values .
Returns :
dict : New merged dictionary combining values from both inputs .
Example :
>> > from copy import deepcopy
>> > default = { " a " : 1 , " nested " : { " x " : 10 , " y " : 20 } }
>> > custom = { " b " : 2 , " nested " : { " y " : 99 , " z " : 30 } }
>> > deep_merge ( default , custom )
{ ' a ' : 1 , ' b ' : 2 , ' nested ' : { ' x ' : 10 , ' y ' : 99 , ' z ' : 30 } }
>> > deep_merge ( { " config " : { " mode " : " auto " } } , { " config " : " manual " } )
{ ' config ' : ' manual ' }
Notes :
1. Merge priority is always given to ` custom ` values at all nesting levels
2. Non - dict values ( e . g . list , str ) in ` custom ` will replace entire values
in ` default ` , even if the original value was a dictionary
3. Time complexity : O ( N ) where N is total key - value pairs in ` custom `
4. Recommended for configuration merging and nested data updates
"""
merged = deepcopy ( default )
stack = [ ( merged , custom ) ]
while stack :
base_dict , override_dict = stack . pop ( )
for key , val in override_dict . items ( ) :
if key in base_dict and isinstance ( val , dict ) and isinstance ( base_dict [ key ] , dict ) :
stack . append ( ( base_dict [ key ] , val ) )
else :
base_dict [ key ] = val
return merged
2025-05-20 09:58:26 +08:00
def remap_dictionary_keys ( source_data : dict , key_aliases : dict = None ) - > dict :
"""
Transform dictionary keys using a configurable mapping schema .
Args :
source_data : Original dictionary to process
key_aliases : Custom key transformation rules ( Optional )
When provided , overrides default key mapping
Format : { < original_key > : < new_key > , . . . }
Returns :
dict : New dictionary with transformed keys preserving original values
Example :
>> > input_data = { " old_key " : " value " , " another_field " : 42 }
>> > remap_dictionary_keys ( input_data , { " old_key " : " new_key " } )
{ ' new_key ' : ' value ' , ' another_field ' : 42 }
"""
DEFAULT_KEY_MAP = {
" chunk_num " : " chunk_count " ,
" doc_num " : " document_count " ,
" parser_id " : " chunk_method " ,
" embd_id " : " embedding_model " ,
}
transformed_data = { }
mapping = key_aliases or DEFAULT_KEY_MAP
for original_key , value in source_data . items ( ) :
mapped_key = mapping . get ( original_key , original_key )
transformed_data [ mapped_key ] = value
return transformed_data
2025-07-15 09:36:45 +08:00
2025-09-29 10:16:13 +08:00
def group_by ( list_of_dict , key ) :
res = { }
for item in list_of_dict :
if item [ key ] in res . keys ( ) :
res [ item [ key ] ] . append ( item )
else :
res [ item [ key ] ] = [ item ]
return res
2025-07-16 18:06:03 +08:00
def get_mcp_tools ( mcp_servers : list , timeout : float | int = 10 ) - > tuple [ dict , str ] :
2025-07-15 09:36:45 +08:00
results = { }
tool_call_sessions = [ ]
try :
for mcp_server in mcp_servers :
server_key = mcp_server . id
cached_tools = mcp_server . variables . get ( " tools " , { } )
tool_call_session = MCPToolCallSession ( mcp_server , mcp_server . variables )
tool_call_sessions . append ( tool_call_session )
try :
tools = tool_call_session . get_tools ( timeout )
except Exception :
tools = [ ]
results [ server_key ] = [ ]
for tool in tools :
tool_dict = tool . model_dump ( )
cached_tool = cached_tools . get ( tool_dict [ " name " ] , { } )
tool_dict [ " enabled " ] = cached_tool . get ( " enabled " , True )
results [ server_key ] . append ( tool_dict )
# PERF: blocking call to close sessions — consider moving to background thread or task queue
close_multiple_mcp_toolcall_sessions ( tool_call_sessions )
return results , " "
except Exception as e :
return { } , str ( e )
2025-07-17 16:48:50 +08:00
async def is_strong_enough ( chat_model , embedding_model ) :
2025-08-04 13:54:18 +08:00
count = settings . STRONG_TEST_COUNT
if not chat_model or not embedding_model :
return
if isinstance ( count , int ) and count < = 0 :
return
2025-08-04 13:34:34 +08:00
@timeout ( 60 , 2 )
2025-07-17 16:48:50 +08:00
async def _is_strong_enough ( ) :
nonlocal chat_model , embedding_model
2025-07-21 15:56:45 +08:00
if embedding_model :
2025-07-23 15:08:36 +08:00
with trio . fail_after ( 10 ) :
2025-07-21 15:56:45 +08:00
_ = await trio . to_thread . run_sync ( lambda : embedding_model . encode ( [ " Are you strong enough!? " ] ) )
if chat_model :
with trio . fail_after ( 30 ) :
2025-11-04 23:47:50 -03:00
res = await trio . to_thread . run_sync ( lambda : chat_model . chat ( " Nothing special. " , [ { " role " : " user " , " content " : " Are you strong enough!? " } ] , { } ) )
2025-07-21 15:56:45 +08:00
if res . find ( " **ERROR** " ) > = 0 :
raise Exception ( res )
2025-07-17 16:48:50 +08:00
# Pressure test for GraphRAG task
async with trio . open_nursery ( ) as nursery :
2025-08-04 13:54:18 +08:00
for _ in range ( count ) :
2025-07-23 09:29:37 +08:00
nursery . start_soon ( _is_strong_enough )
2025-11-04 23:47:50 -03:00
def get_allowed_llm_factories ( ) - > list :
2025-11-05 17:32:12 +08:00
factories = list ( LLMFactoriesService . get_all ( ) )
2025-11-04 23:47:50 -03:00
if settings . ALLOWED_LLM_FACTORIES is None :
2025-11-05 17:32:12 +08:00
return factories
2025-11-04 23:47:50 -03:00
return [ factory for factory in factories if factory . name in settings . ALLOWED_LLM_FACTORIES ]