Skip to content

[OMEGA-427] Use tools API to pass the list of tools to the LLM - #358

Open
vsbogd wants to merge 11 commits into
singnet:mainfrom
vsbogd:use-tools-api
Open

vsbogd wants to merge 11 commits into
singnet:mainfrom
vsbogd:use-tools-api

Conversation

@vsbogd

@vsbogd vsbogd commented Sep 21, 2026 •

Copy link
Copy Markdown
Member

Description

This PR is part of #349 which doesn't include loop and memory changes. It delivers the most massive code change: using tools API to get the list of tool calls from LLM. It obsoletes helper.balance_parenthesis function.

How Has This Been Tested?

This PR doesn't introduce any code logic change just migrates existing logic to the new API. Regression testing should be enough. This PR is checked using automatic regression testing and manual smoke check with each provider.

Checklist

  • PR contains autogenerated code
  • Self-review completed
  • Test scenarios above are passed with the version of the code from PR

Move provider specific logic into prepare_args method mostly. The only
exceptions are OpenAI because it uses completely different method to
call the API and TestMock which doesn't require most of the things.
Change the LLM call API to pass the list of tools to call and receive
the list of tool calls. Adapt loop.metta to this change. Fix unit tests.
The historical information is still returned via HISTORY paragraph of
the system prompt.
@vsbogd
vsbogd marked this pull request as ready for review September 23, 2026 09:03
@vsbogd vsbogd changed the title [OMEGA-374] Use tools API to pass the list of tools to the LLM [OMEGA-427] Use tools API to pass the list of tools to the LLM Sep 24, 2026

@paul-v-snet paul-v-snet left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Other non-critical problems:

Also, most of the issues found during QA in #349 still apply to this PR, so they should be addressed here as well.

Comment thread providers/lib_llm_ext.py

def _log_raw(provider: str, model: str, raw: str) -> None:
logger.debug(f"[LLM_RAW] provider={provider} model={model} chars={len(raw or '')} raw={raw!r}")
def _log_raw(kind, provider: str, model: str, raw: Dict) -> None:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

non-critical: kind without annotation

suggesting to replace it with: kind: str

Comment thread providers/openai.py
raw = llm._llm_empty_response_command()
return self._clean_text(raw)
if incomplete_reason == "max_output_tokens":
response.add_tool_call(_llm_empty_response_tool_call(raw.id))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

critical: _llm_empty_response_tool_call defined in providers/lib_llm_ext.py, so it will lead to an error.

suggesting to replace it with: response.add_tool_call(llm._llm_empty_response_tool_call(raw.id))

Comment thread providers/lib_llm_ext.py
}

def convert_response(self, raw):
response = LLMResponse()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

non-critical: double spaces

Comment thread src/providers.metta
Comment on lines +7 to +14
(= (llmRequestMessage role $role content $content)
(py-call (providers.llmRequestMessage $role $content)))

(= (llmRequestMessage role $role callid $callid content $content)
(py-call (providers.llmToolCallResponseMessage $role $callid $content)))

(= (llmRequestMessage role $role calls $calls content $content)
(py-call (providers.llmToolCallMessage $role $calls $content)))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

non-critical

I'm afraid this may be confusing. The same expression llmRequestMessage refers to several different things at the same time:

  1. User's request to the LLM
  2. LLM's response
  3. Tool call results

So I believe it would be better to use different names for them, but since this is not critical, it would be enough to just take this into account for the future.

Comment thread src/providers.py
Comment on lines +138 to +139
self.max_tokens = 6000
self.reasoning_mode = "medium"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

non-critical, but important

This may lead to hidden issues in the future since max_tokens and reasoning_mode are hardcoded here. If for any reason these values are not passed explicitly, they will be silently configured using these hardcoded values rather than the values from config.yaml.

So I suggest using config_get_by_key here instead of any hardcoded values.

Comment thread src/providers.py
Comment on lines +242 to +253
def _validate_response(request: LLMRequest, response: LLMResponse) -> LLMResponse:
for call in response.calls:
if call.is_error():
continue
if not request.has_tool(call.name):
call.set_error(f"Unknown tool: {call.name!r}")
call.set_tool(request.get_tool(call.name))
for parameter in call.tool.parameters:
if not parameter.name in call.arguments:
call.set_error(f"Call tool parameter is not set: tool: {call.name!r}, parameter: {parameter.name!r}")
break
return response

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

potentially critical:

There is no continue after call.set_error(f"Unknown tool: {call.name!r}"), so call.set_tool(request.get_tool(call.name)) at line 248 will return None, and for parameter in call.tool.parameters: at line 249 will raise an AttributeError.

As a side effect, the LLM will never receive the Unknown tool message and will not be able to correct itself in subsequent responses.

Comment thread src/providers.py
Comment on lines +311 to +321
def llmToolCallToSExpr(call: LLMToolCall):
sexpr = f"({call.name} "
for parameter in call.tool.parameters:
if parameter.name in call.arguments:
arg = call.arguments[parameter.name]
arg = arg.replace('"','\\"')
sexpr = sexpr + f"\"{arg}\" "
sexpr = sexpr + ")"
if call.is_error():
sexpr = f"(Error {sexpr} \"{call.error}\")"
return f"({call.id} {sexpr})"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Same as what @TossSky found during QA in #349, point 8:

  1. llmToolCallToSExpr escapes " but not \ (providers.py:316), so ls C:\ becomes (shell "ls C:") and the backslash escapes the closing quote. A literal \n in an argument turns into a newline. A non-string argument raises AttributeError at loop.metta:79, outside the catch on line 83.

"\nResults in " (researchDir) "/" $research-name "/"
"\nWorkflow unloaded from context.")))
(appendToHistory ((get_time_as_string) (newline) (strings-concat ("RESEARCH [" $research-name "] COMPLETED")) (newline)) )
(addToHistory (strings-concat ("RESEARCH [" $research-name "] COMPLETED")))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

potentially critical:

addToHistory expects 3 arguments, but the workflow plugins provide only 1 everywhere.

I guess this will lead to errors or these expressions being silently ignored (I'm not sure how this will be processed by PeTTa).

Anyway, I'd like to ask @TossSky to check this during QA later.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants