2024-06-14 10:49: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-12-11 17:38:17 +08:00
import asyncio
2024-11-14 17:13:48 +08:00
import logging
2025-07-30 19:41:09 +08:00
import os
import re
2024-06-14 10:49:36 +08:00
from abc import ABC
2025-07-30 19:41:09 +08:00
2025-11-05 08:01:39 +08:00
from common . constants import LLMType
2024-06-14 10:49:36 +08:00
from api . db . services . llm_service import LLMBundle
2026-07-09 14:02:08 +08:00
from api . db . joint_services . tenant_model_service import resolve_model_config
2025-07-31 15:13:45 +08:00
from agent . component . llm import LLMParam , LLM
2025-11-04 11:51:12 +08:00
from common . connection_utils import timeout
2025-07-30 19:41:09 +08:00
from rag . llm . chat_model import ERROR_PREFIX
2024-06-14 10:49:36 +08:00
2025-07-30 19:41:09 +08:00
class CategorizeParam ( LLMParam ) :
2024-06-14 10:49:36 +08:00
"""
2025-09-25 12:05:43 +08:00
Define the categorize component parameters.
2024-06-14 10:49:36 +08:00
"""
2026-07-03 12:53:39 +08:00
2024-06-14 10:49:36 +08:00
def __init__ ( self ) :
super ( ) . __init__ ( )
self . category_description = { }
2025-07-30 19:41:09 +08:00
self . query = " sys.query "
self . message_history_window_size = 1
self . update_prompt ( )
2024-06-14 10:49:36 +08:00
def check ( self ) :
2026-06-11 01:24:48 -04:00
if not isinstance ( self . message_history_window_size , int ) or self . message_history_window_size < 0 :
raise ValueError ( " [Categorize] Message window size cannot be negative " )
2024-07-08 09:32:44 +08:00
self . check_empty ( self . category_description , " [Categorize] Category examples " )
for k , v in self . category_description . items ( ) :
2024-12-08 14:21:12 +08:00
if not k :
raise ValueError ( " [Categorize] Category name can not be empty! " )
if not v . get ( " to " ) :
raise ValueError ( f " [Categorize] ' To ' of category { k } can not be empty! " )
2024-06-14 10:49:36 +08:00
2025-07-30 19:41:09 +08:00
def get_input_form ( self ) - > dict [ str , dict ] :
2026-07-03 12:53:39 +08:00
return { " query " : { " type " : " line " , " name " : " Query " } }
2025-07-30 19:41:09 +08:00
def update_prompt ( self ) :
2024-06-14 10:49:36 +08:00
cate_lines = [ ]
for c , desc in self . category_description . items ( ) :
2025-07-30 19:41:09 +08:00
for line in desc . get ( " examples " , [ ] ) :
2024-12-08 14:21:12 +08:00
if not line :
continue
2026-07-03 12:53:39 +08:00
cate_lines . append ( ' USER: " ' + re . sub ( r " \ n " , " " , line , flags = re . DOTALL ) + ' " → ' + c )
2025-07-30 19:41:09 +08:00
2024-06-14 10:49:36 +08:00
descriptions = [ ]
for c , desc in self . category_description . items ( ) :
if desc . get ( " description " ) :
2026-07-03 12:53:39 +08:00
descriptions . append ( " \n ------ \n Category: {} \n Description: {} " . format ( c , desc [ " description " ] ) )
2024-06-14 10:49:36 +08:00
2025-07-30 19:41:09 +08:00
self . sys_prompt = """
You are an advanced classification system that categorizes user questions into specific types. Analyze the input question and classify it into ONE of the following categories:
{}
2024-06-14 10:49:36 +08:00
2025-03-21 16:22:03 +08:00
Here ' s description of each category:
2025-07-30 19:41:09 +08:00
- {}
---- Instructions ----
- Consider both explicit mentions and implied context
- Prioritize the most specific applicable category
- Return only the category name without explanations
- Use " Other " only when no other category fits
2025-09-25 14:11:09 +08:00
2026-07-03 12:53:39 +08:00
""" . format ( " \n - " . join ( list ( self . category_description . keys ( ) ) ) , " \n " . join ( descriptions ) )
2025-03-21 16:22:03 +08:00
2025-07-30 19:41:09 +08:00
if cate_lines :
self . sys_prompt + = """
---- Examples ----
2025-03-21 16:22:03 +08:00
{}
2025-07-30 19:41:09 +08:00
""" . format ( " \n " . join ( cate_lines ) )
2025-03-21 16:22:03 +08:00
2025-07-30 19:41:09 +08:00
class Categorize ( LLM , ABC ) :
2024-06-14 10:49:36 +08:00
component_name = " Categorize "
2026-01-13 17:54:57 +08:00
def get_input_elements ( self ) - > dict [ str , dict ] :
query_key = self . _param . query or " sys.query "
elements = self . get_input_elements_from_text ( f " {{ { query_key } }} " )
if not elements :
logging . warning ( f " [Categorize] input element not detected for query key: { query_key } " )
return elements
2026-07-03 12:53:39 +08:00
@timeout ( int ( os . environ . get ( " COMPONENT_EXEC_TIMEOUT " , 10 * 60 ) ) )
2025-12-11 17:38:17 +08:00
async def _invoke_async ( self , * * kwargs ) :
2025-11-11 17:36:48 +08:00
if self . check_if_canceled ( " Categorize processing " ) :
return
2025-07-30 19:41:09 +08:00
msg = self . _canvas . get_history ( self . _param . message_history_window_size )
if not msg :
msg = [ { " role " : " user " , " content " : " " } ]
2026-01-13 17:54:57 +08:00
query_key = self . _param . query or " sys.query "
if query_key in kwargs :
query_value = kwargs [ query_key ]
2025-07-30 19:41:09 +08:00
else :
2026-01-13 17:54:57 +08:00
query_value = self . _canvas . get_variable_value ( query_key )
if query_value is None :
query_value = " "
msg [ - 1 ] [ " content " ] = query_value
self . set_input_value ( query_key , msg [ - 1 ] [ " content " ] )
2025-07-30 19:41:09 +08:00
self . _param . update_prompt ( )
2026-07-09 14:02:08 +08:00
chat_model_config = resolve_model_config ( self . _canvas . get_tenant_id ( ) , LLMType . CHAT , self . _param . llm_id )
2026-03-05 17:27:17 +08:00
chat_mdl = LLMBundle ( self . _canvas . get_tenant_id ( ) , chat_model_config )
2025-05-12 16:15:19 +07:00
2025-07-30 19:41:09 +08:00
user_prompt = """
---- Real Data ----
2025-09-25 14:11:09 +08:00
{} →
2026-07-03 12:53:39 +08:00
""" . format ( " | " . join ( [ ' {} : " {} " ' . format ( c [ " role " ] . upper ( ) , re . sub ( r " \ n " , " " , c [ " content " ] , flags = re . DOTALL ) ) for c in msg ] ) )
2025-11-11 17:36:48 +08:00
if self . check_if_canceled ( " Categorize processing " ) :
return
2025-12-11 17:38:17 +08:00
ans = await chat_mdl . async_chat ( self . _param . sys_prompt , [ { " role " : " user " , " content " : user_prompt } ] , self . _param . gen_conf ( ) )
2025-07-30 19:41:09 +08:00
logging . info ( f " input: { user_prompt } , answer: { str ( ans ) } " )
if ERROR_PREFIX in ans :
raise Exception ( ans )
2025-11-11 17:36:48 +08:00
if self . check_if_canceled ( " Categorize processing " ) :
return
2025-03-27 17:02:21 +08:00
# Count the number of times each category appears in the answer.
category_counts = { }
2024-06-14 10:49:36 +08:00
for c in self . _param . category_description . keys ( ) :
2025-03-27 17:02:21 +08:00
count = ans . lower ( ) . count ( c . lower ( ) )
category_counts [ c ] = count
2025-07-30 19:41:09 +08:00
cpn_ids = list ( self . _param . category_description . items ( ) ) [ - 1 ] [ 1 ] [ " to " ]
2026-01-23 12:54:08 +08:00
max_category = list ( self . _param . category_description . keys ( ) ) [ - 1 ]
2025-03-27 17:02:21 +08:00
if any ( category_counts . values ( ) ) :
2025-07-30 19:41:09 +08:00
max_category = max ( category_counts . items ( ) , key = lambda x : x [ 1 ] ) [ 0 ]
cpn_ids = self . _param . category_description [ max_category ] [ " to " ]
self . set_output ( " category_name " , max_category )
self . set_output ( " _next " , cpn_ids )
2024-06-14 10:49:36 +08:00
2026-07-03 12:53:39 +08:00
@timeout ( int ( os . environ . get ( " COMPONENT_EXEC_TIMEOUT " , 10 * 60 ) ) )
2025-12-11 17:38:17 +08:00
def _invoke ( self , * * kwargs ) :
return asyncio . run ( self . _invoke_async ( * * kwargs ) )
2025-07-31 15:13:45 +08:00
def thoughts ( self ) - > str :
2025-09-25 14:11:09 +08:00
return " Which should it falls into {} ? ... " . format ( " , " . join ( [ f " ` { c } ` " for c , _ in self . _param . category_description . items ( ) ] ) )