Skip to content

Commit 2fac373

Browse files
committed
message handling migration
1 parent 1cadecc commit 2fac373

1 file changed

Lines changed: 148 additions & 47 deletions

File tree

src/text_message_handler.py

Lines changed: 148 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -132,55 +132,82 @@ def get_today_usage():
132132
return daily_usage # daily_usage is (premium_tokens, mini_tokens) or None
133133

134134
# get model family and build payload
135-
def is_gpt4_family_model(model: str) -> bool:
135+
def model_supports_temperature(model: str) -> bool:
136136
"""
137137
Harry rule:
138-
Only gpt-4* models get legacy Chat Completions sampling/function payloads.
139-
Examples allowed:
140-
gpt-4
141-
gpt-4.1
142-
gpt-4o
143-
gpt-4-turbo
144-
145-
Everything else:
146-
no temperature
147-
no old functions/function_call payload
138+
Only gpt-4* models get temperature.
139+
GPT-5.x / other newer reasoning-ish models do NOT get temperature.
148140
"""
149141
model = (model or "").strip().lower()
150142
return model.startswith("gpt-4")
151143

144+
145+
def build_openai_tools(function_specs):
146+
"""
147+
Convert legacy ChatKeke custom_functions format:
148+
149+
{"name": "...", "description": "...", "parameters": {...}}
150+
151+
into modern Chat Completions tools format:
152+
153+
{"type": "function", "function": {...}}
154+
155+
If already in tools format, keep it.
156+
"""
157+
tools = []
158+
159+
for item in function_specs or []:
160+
if isinstance(item, dict) and item.get("type") == "function" and "function" in item:
161+
tools.append(item)
162+
else:
163+
tools.append({
164+
"type": "function",
165+
"function": item
166+
})
167+
168+
return tools
169+
170+
152171
def build_chat_payload(bot, messages, *, include_functions=True, max_tokens=None):
153172
"""
154173
Centralized Chat Completions payload builder.
155174
156175
Important:
157-
- Only gpt-4* models receive temperature.
158-
- Only gpt-4* models receive old-style functions/function_call.
159-
- Non-4 models become plain text Chat Completions calls.
176+
- GPT-4* gets temperature.
177+
- Non-GPT-4 models do NOT get temperature.
178+
- Function calling uses modern tools/tool_choice for all models.
160179
"""
161180
payload = {
162181
"model": bot.model,
163182
"messages": messages,
164183
}
165184

166185
if max_tokens is not None:
167-
if is_gpt4_family_model(bot.model):
186+
if model_supports_temperature(bot.model):
168187
payload["max_tokens"] = max_tokens
169188
else:
170189
payload["max_completion_tokens"] = max_tokens
171190

172-
if is_gpt4_family_model(bot.model):
191+
if model_supports_temperature(bot.model):
173192
payload["temperature"] = bot.temperature
174-
175-
if include_functions:
176-
payload["functions"] = custom_functions
177-
payload["function_call"] = "auto"
178193
else:
179194
bot.logger.info(
180-
"Model %s is not gpt-4*; omitting temperature and legacy function_call/functions.",
195+
"Model %s is not gpt-4*; omitting temperature.",
181196
bot.model
182197
)
183198

199+
if include_functions:
200+
tools = build_openai_tools(custom_functions)
201+
if tools:
202+
payload["tools"] = tools
203+
payload["tool_choice"] = "auto"
204+
if include_functions:
205+
tools = build_openai_tools(custom_functions)
206+
if tools:
207+
payload["tools"] = tools
208+
payload["tool_choice"] = "auto"
209+
payload["parallel_tool_calls"] = False
210+
184211
return payload
185212

186213
def extract_chat_reply_or_raise(response_json):
@@ -204,6 +231,51 @@ def extract_chat_reply_or_raise(response_json):
204231
content = message.get("content") or ""
205232
return content.strip()
206233

234+
def extract_function_call_or_none(response_json):
235+
"""
236+
Supports both:
237+
- modern Chat Completions tool_calls
238+
- old legacy function_call
239+
240+
Returns:
241+
{"name": str, "arguments": str, "tool_call_id": str|None}
242+
or None.
243+
"""
244+
if not isinstance(response_json, dict):
245+
raise RuntimeError(f"OpenAI response was not a JSON object: {response_json!r}")
246+
247+
if "error" in response_json:
248+
raise RuntimeError(f"OpenAI API error: {response_json['error']}")
249+
250+
choices = response_json.get("choices") or []
251+
if not choices:
252+
raise RuntimeError(f"OpenAI response had empty choices: {response_json}")
253+
254+
message_obj = choices[0].get("message") or {}
255+
256+
# Modern tools/tool_calls path.
257+
tool_calls = message_obj.get("tool_calls") or []
258+
if tool_calls:
259+
first_tool_call = tool_calls[0]
260+
if first_tool_call.get("type") == "function":
261+
fn = first_tool_call.get("function") or {}
262+
return {
263+
"name": fn.get("name"),
264+
"arguments": fn.get("arguments") or "{}",
265+
"tool_call_id": first_tool_call.get("id"),
266+
}
267+
268+
# Legacy functions/function_call path.
269+
legacy_function_call = message_obj.get("function_call")
270+
if legacy_function_call:
271+
return {
272+
"name": legacy_function_call.get("name"),
273+
"arguments": legacy_function_call.get("arguments") or "{}",
274+
"tool_call_id": None,
275+
}
276+
277+
return None
278+
207279
# model picker auto-switch
208280
def pick_model_auto_switch(bot):
209281
if not config_auto.has_section('ModelAutoSwitch'):
@@ -327,10 +399,9 @@ async def handle_message(bot, update: Update, context: CallbackContext, logger)
327399
bot.logger.info(f"[Debug] Total token usage from file: {bot.total_token_usage}")
328400

329401
except ValueError:
330-
# Handle the case where the value in config.ini is not a valid integer
331402
bot.logger.error("Invalid value for GlobalMaxTokenUsagePerDay in the configuration file.")
332403
await update.message.reply_text("An error occurred while processing your request: couldn't get proper token count. Please try again later.")
333-
# max_tokens_config = 0 # Assign a default value (0 or any other appropriate default)
404+
return
334405

335406
# Safely compare user_token_count and max_tokens_config
336407
if not is_no_limit and (bot.total_token_usage + user_token_count) > max_tokens_config:
@@ -657,15 +728,10 @@ async def handle_message(bot, update: Update, context: CallbackContext, logger)
657728
# > function calling
658729
# ~~~~~~~~~~~~~~~~~~
659730

660-
# Check for a 'function_call' in the response
661-
choices = response_json.get("choices") or []
662-
if not choices:
663-
raise RuntimeError(f"OpenAI response had empty choices: {response_json}")
731+
# Check for a modern tool_call or legacy function_call in the response
732+
function_call = extract_function_call_or_none(response_json)
664733

665-
message_obj = choices[0].get("message") or {}
666-
667-
if "function_call" in message_obj:
668-
function_call = message_obj["function_call"]
734+
if function_call:
669735
function_name = function_call["name"]
670736

671737
# ~~~~~~~~~~~~~~~~~~~~~~
@@ -720,7 +786,12 @@ async def handle_message(bot, update: Update, context: CallbackContext, logger)
720786
bot.logger.info(f"Updated chat history with calculator result: {chat_history}")
721787

722788
# Make an API request using the updated chat history
723-
response_json = await make_api_request(bot, chat_history, bot.timeout)
789+
response_json = await make_api_request(
790+
bot,
791+
chat_history,
792+
bot.timeout,
793+
include_functions=False
794+
)
724795

725796
# Extract and handle the content from the API response
726797
bot_reply_content = extract_chat_reply_or_raise(response_json)
@@ -786,7 +857,12 @@ async def handle_message(bot, update: Update, context: CallbackContext, logger)
786857
context.chat_data['chat_history'] = chat_history
787858

788859
# Prepare the payload for the API request with updated chat history
789-
response_json = await make_api_request(bot, chat_history, bot.timeout)
860+
response_json = await make_api_request(
861+
bot,
862+
chat_history,
863+
bot.timeout,
864+
include_functions=False
865+
)
790866

791867
# Log the API request payload
792868
bot.logger.info(f"API Request Payload: {payload}")
@@ -795,9 +871,7 @@ async def handle_message(bot, update: Update, context: CallbackContext, logger)
795871
bot_reply_content = extract_chat_reply_or_raise(response_json)
796872

797873
# Only call strip if bot_reply_content is not None
798-
bot_reply = "" # Default value if no content is found or an empty response is received
799-
if bot_reply_content:
800-
bot_reply = bot_reply_content.strip()
874+
bot_reply = bot_reply_content.strip() if bot_reply_content else "🤔"
801875

802876
# Count tokens in the bot's response
803877
bot_token_count = bot.count_tokens(bot_reply)
@@ -874,7 +948,12 @@ async def handle_message(bot, update: Update, context: CallbackContext, logger)
874948
bot.logger.info(f"Updated chat history: {chat_history}")
875949

876950
# Make an API request using the updated chat history
877-
response_json = await make_api_request(bot, chat_history, bot.timeout)
951+
response_json = await make_api_request(
952+
bot,
953+
chat_history,
954+
bot.timeout,
955+
include_functions=False
956+
)
878957

879958
# Extract and handle the content from the API response
880959
bot_reply_content = extract_chat_reply_or_raise(response_json)
@@ -952,7 +1031,12 @@ async def handle_message(bot, update: Update, context: CallbackContext, logger)
9521031
context.chat_data['chat_history'] = chat_history
9531032

9541033
# Make an API request using the updated chat history
955-
response_json = await make_api_request(bot, chat_history, bot.timeout)
1034+
response_json = await make_api_request(
1035+
bot,
1036+
chat_history,
1037+
bot.timeout,
1038+
include_functions=False
1039+
)
9561040

9571041
# Extract and handle the content from the API response
9581042
bot_reply_content = extract_chat_reply_or_raise(response_json)
@@ -1036,7 +1120,12 @@ async def handle_message(bot, update: Update, context: CallbackContext, logger)
10361120
context.chat_data['chat_history'] = chat_history
10371121

10381122
# Make the API request using the new function
1039-
response_json = await make_api_request(bot, chat_history, bot.timeout)
1123+
response_json = await make_api_request(
1124+
bot,
1125+
chat_history,
1126+
bot.timeout,
1127+
include_functions=False
1128+
)
10401129

10411130
# Log the API request payload
10421131
bot.logger.info(f"API Request Payload: {payload}")
@@ -1045,9 +1134,7 @@ async def handle_message(bot, update: Update, context: CallbackContext, logger)
10451134
bot_reply_content = extract_chat_reply_or_raise(response_json)
10461135

10471136
# Only call strip if bot_reply_content is not None
1048-
bot_reply = "" # Default value if no content is found or an empty response is received
1049-
if bot_reply_content:
1050-
bot_reply = bot_reply_content.strip()
1137+
bot_reply = bot_reply_content.strip() if bot_reply_content else "🤔"
10511138

10521139
# Count tokens in the bot's response
10531140
bot_token_count = bot.count_tokens(bot_reply)
@@ -1233,7 +1320,12 @@ async def handle_message(bot, update: Update, context: CallbackContext, logger)
12331320
bot.logger.info(f"Updated chat history: {chat_history}")
12341321

12351322
# Make an API request using the updated chat history
1236-
response_json = await make_api_request(bot, chat_history, bot.timeout)
1323+
response_json = await make_api_request(
1324+
bot,
1325+
chat_history,
1326+
bot.timeout,
1327+
include_functions=False
1328+
)
12371329

12381330
# Extract and handle the content from the API response
12391331
bot_reply_content = extract_chat_reply_or_raise(response_json)
@@ -1367,7 +1459,12 @@ async def handle_message(bot, update: Update, context: CallbackContext, logger)
13671459

13681460
# (D) Re-invoke GPT to produce final user-facing text
13691461
context.chat_data['chat_history'] = chat_history
1370-
response_json = await make_api_request(bot, chat_history, bot.timeout)
1462+
response_json = await make_api_request(
1463+
bot,
1464+
chat_history,
1465+
bot.timeout,
1466+
include_functions=False
1467+
)
13711468

13721469
final_reply_content = extract_chat_reply_or_raise(response_json)
13731470
final_reply = final_reply_content.strip() if final_reply_content else ""
@@ -1656,7 +1753,7 @@ async def generate_response_based_on_updated_context(bot, context, chat_id):
16561753

16571754
# Extract the generated response from the response data.
16581755
try:
1659-
generated_response = extract_chat_reply_or_raise(response_data)
1756+
generated_response = extract_chat_reply_or_raise(response_data) or "🤔"
16601757
except Exception as e:
16611758
logging.error(f"Fallback OpenAI response was invalid: {e}")
16621759
generated_response = "🤔"
@@ -1707,9 +1804,13 @@ async def make_api_request_with_retry(bot, chat_history, retries=3, timeout=30):
17071804
raise RuntimeError("All retry attempts failed; no response_json was produced.")
17081805

17091806
# API request function module
1710-
async def make_api_request(bot, chat_history, timeout=30):
1807+
async def make_api_request(bot, chat_history, timeout=30, include_functions=True):
17111808
# Prepare the payload for the API request with updated chat history
1712-
payload = build_chat_payload(bot, chat_history)
1809+
payload = build_chat_payload(
1810+
bot,
1811+
chat_history,
1812+
include_functions=include_functions
1813+
)
17131814

17141815
# Make the API request
17151816
headers = {

0 commit comments

Comments
 (0)