Skip to content
Closed
Changes from 18 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
70902ed
improvements to tool calling logic
Nov 7, 2025
1452960
Revert "improvements to tool calling logic"
Nov 7, 2025
72a96fa
major improvements to tool calling logic
Nov 7, 2025
a86080d
major improvements to tool calling logic
Nov 7, 2025
7329347
fixed toolcall choosing logic
Nov 7, 2025
0c69dfd
fixed toolcall choosing logic even more
Nov 7, 2025
b8a4a08
removed concedo's toolcall escape since it clashes with my prompting
Nov 7, 2025
afba8bd
improved tool call choice logic by taking into account LLM's prior re…
Nov 7, 2025
f0b32b3
improved interpreting of first decision
Nov 7, 2025
86b8317
improved tool call decision logic by putting the user's LAST message …
Nov 7, 2025
7bf8ffc
fixed small mistake
Nov 7, 2025
5a9347d
fixed formatting issue
Nov 7, 2025
58f37f4
Merge branch 'LostRuins:concedo_experimental' into concedo_experimental
Rose22 Nov 7, 2025
994427d
linting
LostRuins Nov 10, 2025
ba20f9f
Merge branch 'LostRuins:concedo_experimental' into concedo_experimental
Rose22 Nov 10, 2025
44d0166
Merge branch 'LostRuins:concedo_experimental' into concedo_experimental
Rose22 Nov 10, 2025
1794c70
Merge branch 'LostRuins:concedo_experimental' into concedo_experimental
Rose22 Nov 10, 2025
9a4a4d0
Merge branch 'LostRuins:concedo_experimental' into concedo_experimental
Rose22 Nov 10, 2025
459b39b
Merge branch 'LostRuins:concedo_experimental' into concedo_experimental
Rose22 Nov 11, 2025
edba5ae
Merge branch 'LostRuins:concedo_experimental' into concedo_experimental
Rose22 Nov 18, 2025
47c4809
added some tweaks for improved tool calls to reuse old ctx, but needs…
LostRuins Nov 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 103 additions & 24 deletions koboldcpp.py
Original file line number Diff line number Diff line change
Expand Up @@ -2495,38 +2495,118 @@ def determine_tool_json_to_use(genparams, curr_ctx, assistant_message_start, is_
# tools handling: Check if user is passing a openai tools array, if so add to end of prompt before assistant prompt unless tool_choice has been set to None
tools_array = genparams.get('tools', [])
chosen_tool = genparams.get('tool_choice', "auto")
messages = genparams.get("messages")

# first handle auto mode, determine whether a tool is needed
used_tool_json = None
if not curr_ctx:

if not curr_ctx or not messages:
return None

if tools_array and len(tools_array) > 0 and chosen_tool is not None and chosen_tool!="none":
tools_string = json.dumps(tools_array, indent=0)
# extract the last 6 user messages and AI responses
# from the messages array
messages_truncated = messages[-6:]

# get user's last message
last_user_message = ""
for message in messages_truncated:
if message['role'] == "user":
last_user_message = message['content']

# get last tool call results
tool_call_results = []
for message in reversed(messages_truncated):
# we get only the tool call results
# since the last user request
if message['role'] == "tool":
tool_call_results.append(message['content'])
else:
break
tool_call_results = list(reversed(tool_call_results))

# pass only the essential tool call information
# to the model, to reduce the size of the prompt
# it needs to process
tools_array_filtered = []
for tool_dict in tools_array:
tool_data = tool_dict['function']

tool_props = {}
for prop_name, prop_data in tool_data['parameters']['properties'].items():
tool_props[prop_name] = prop_data['type']

tools_array_filtered.append({
"name": tool_data['name'],
"description": tool_data['description'],
"properties": tool_props
})

tools_string = json.dumps(tools_array_filtered, indent=0)

should_use_tools = True
if chosen_tool=="auto":
# if you want a different template, you can set 'custom_tools_prompt' in the chat completions adapter as follows
custom_tools_prompt = "Can the user query be answered by a listed tool above? (One word response: yes or no):"
if is_followup_tool:
custom_tools_prompt = "Can the user query be further answered by another listed tool above? (If response is already complete, reply NO) (One word response: yes or no):"
# note: message string already contains the instruct start tag!

pollgrammar = r'root ::= "yes" | "no" | "Yes" | "No" | "YES" | "NO"'

if not is_followup_tool:
# if you want a different template, you can set 'custom_tools_prompt' in the chat completions adapter as follows
custom_tools_prompt = "Is one of the tool calls listed above absolutely essential to answer user's request, or is a tool call optional? Explain your reasoning in one sentence. State your final decision at the end. Don't use emojis."
custom_tools_prompt_processed = f"User's request: {last_user_message}\n\nChat history: {messages_truncated}\n\nTool List:\n{tools_string}\n\n{custom_tools_prompt}{assistant_message_start}"
else:
custom_tools_prompt = "If user's request was to generate any kind of non-text media, no further action is needed and the answer should be no, regardless of what the tool call response was. Otherwise, given the tool call response to the user's request, is another tool call needed to further answer user's message? State your final decision at the end. Don't use emojis."
custom_tools_prompt_processed = f"User's request: {last_user_message}\n\nTool call responses: {tool_call_results}\n\nTool List:\n{tools_string}\n\n{custom_tools_prompt}{assistant_message_start}"

# first, prompt to see if a tool call is needed using the prompt above.
# the result is a short explanation by the LLM on why a tool call
# is or is not needed, along with it's final decision at the end.
temp_poll = {
"prompt": f"{curr_ctx}\n\nTool List:\n{tools_string}\n\n{custom_tools_prompt}{assistant_message_start}",
"prompt": custom_tools_prompt_processed,
"max_length":500,
"temperature":0.1,
"top_k":1,
"rep_pen":1,
"ban_eos_token":False
}
temp_poll_result = generate(genparams=temp_poll)
temp_poll_text = temp_poll_result['text'].strip().rstrip('.')

# then we take that final decision
# and translate it to a simple "yes" or "no" using
# another call to the model
temp_poll_check = {
"prompt": f"LLM's reasoning: {temp_poll_text}\n\nDid the LLM's final decision state a tool call is needed? (one word answer: yes or no)",
"max_length":5,
"temperature":0.1,
"top_k":1,
"rep_pen":1,
"ban_eos_token":False,
"grammar":pollgrammar
}
temp_poll_result = generate(genparams=temp_poll)
if temp_poll_result and "yes" not in temp_poll_result['text'].lower():
"grammar": pollgrammar
}
temp_poll_check_result = generate(genparams=temp_poll_check)
temp_poll_check_text = temp_poll_check_result['text'].lower()

if temp_poll_result and "yes" not in temp_poll_check_text:
should_use_tools = False

if not args.quiet:
print(f"\nRelevant tool is listed: {temp_poll_result['text']} ({should_use_tools})")
print()
print("[TOOLCALL REQUEST]")
print(f"Request: {last_user_message}")
print(f"Prompt: {custom_tools_prompt}")
if is_followup_tool:
print(f"Previous tool call results: {tool_call_results}")
print(f"Decision: {temp_poll_check_text}")
print(f"Reasoning: {temp_poll_text}")

if chosen_tool != "auto":
print(f"Chosen tool: {chosen_tool}")

if should_use_tools:
#first, try and extract a specific tool if selected
used_tool_json = extract_tool_info_from_tool_array(chosen_tool, tools_array)

if used_tool_json: #already found the tool we want, remove all others
pass
elif len(tools_array)==1:
Expand All @@ -2540,11 +2620,11 @@ def determine_tool_json_to_use(genparams, curr_ctx, assistant_message_start, is_
for name in toolnames:
pollgrammar += ("" if pollgrammar=="" else " | ")
pollgrammar += "\"" + name + "\""
pollgrammar += " | \"no_tool\""
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why remove the null tool? seems like its still a good idea to have even if the llm knows that a tool is needed. it gives it a second chance to change its mind.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

because in testing this often resulted in toolcalls that i explicitely asked for being cancelled. best to just not interfere with its final reasoning..

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Formerly the null tool was to give the LLM an out they would go for, rosie's PR instead does this with a reasoning step. Same concept different implementation.

pollgrammar = r'root ::= ' + pollgrammar
decide_tool_prompt = "Which of the listed tools should be used next? Pick exactly one. If no tool is suitable, reply no_tool. (Reply directly with the selected tool's name):"

decide_tool_prompt = "Which of the listed tools should be used next? Pick exactly one. If the LLM reasoning includes a suggested tool to call, select that one. (Reply directly with the selected tool's name):"
temp_poll = {
"prompt": f"{curr_ctx}\n\nTool List:\n{tools_string}\n\n{decide_tool_prompt}{assistant_message_start}",
"prompt": f"Chat history: {messages_truncated}\n\nPrevious toolcall responses: {tool_call_results}\n\nLLM's reasoning: {temp_poll_text}\n\nTool List:\n{tools_string}\n\n{decide_tool_prompt}{assistant_message_start}",
"max_length":16,
"temperature":0.1,
"top_k":1,
Expand All @@ -2553,17 +2633,16 @@ def determine_tool_json_to_use(genparams, curr_ctx, assistant_message_start, is_
"grammar":pollgrammar
}
temp_poll_result = generate(genparams=temp_poll)

if temp_poll_result:
raw = temp_poll_result['text'].lower()
if "no_tool" in raw:
print(f"\nNo suitable tool found.")
else:
for name in toolnames:
if name.lower() in raw:
used_tool_json = extract_tool_info_from_tool_array(name, tools_array)
if not args.quiet:
print(f"\nAttempting to use tool: {name}")
break

for name in toolnames:
if name.lower() in raw:
used_tool_json = extract_tool_info_from_tool_array(name, tools_array)
if not args.quiet:
print(f"\n\nAttempting to use tool: {name}\n")
break

return used_tool_json

Expand Down