Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .github/workflows/coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,5 @@ jobs:
python_version: '3.11'
coverage_source: 'ovos_agentic_loop'
test_path: 'test/'
install_extras: ''
test_extras: 'test' # install .[test] (pulls the ovos-openai-plugin e2e brain)
min_coverage: 0
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Changelog

## [0.2.0a1](https://github.com/OpenVoiceOS/ovos-agentic-loop/tree/0.2.0a1) (2026-06-20)

[Full Changelog](https://github.com/OpenVoiceOS/ovos-agentic-loop/compare/0.1.1a1...0.2.0a1)

**Merged pull requests:**

- feat: NativeToolCallEngine — provider-native tool-calling loop [\#5](https://github.com/OpenVoiceOS/ovos-agentic-loop/pull/5) ([JarbasAl](https://github.com/JarbasAl))

## [0.1.1a1](https://github.com/OpenVoiceOS/ovos-agentic-loop/tree/0.1.1a1) (2026-06-10)

[Full Changelog](https://github.com/OpenVoiceOS/ovos-agentic-loop/compare/0.1.0...0.1.1a1)

**Merged pull requests:**

- fix: make SkillMDLoader tests hermetic [\#2](https://github.com/OpenVoiceOS/ovos-agentic-loop/pull/2) ([JarbasAl](https://github.com/JarbasAl))



\* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)*
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,3 +155,11 @@ messages = ctx.build_conversation_context(utterance, session_id="s1")
## License

Apache 2.0 — see [LICENSE](LICENSE).

## Credits

Developed by [TigreGotico](https://tigregotico.pt) for [OpenVoiceOS](https://openvoiceos.org).

Funded by [NGI0 Commons Fund](https://nlnet.nl/project/OpenVoiceOS) / [NLnet](https://nlnet.nl)
under grant agreement No [101135429](https://cordis.europa.eu/project/id/101135429),
through the European Commission's [Next Generation Internet](https://ngi.eu) programme.
2 changes: 2 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ All components integrate with `ovos-plugin-manager` (OPM) via entry points and a
| `AgenticLoopEngine` | `ovos_agentic_loop/base.py:8` | — (abstract base) | — |
| `ReActLoopEngine` | `ovos_agentic_loop/react.py:92` | — (concrete) | — |
| `ReActLoopEnginePlugin` | `ovos_agentic_loop/factory.py:8` | `opm.agents.chat` | `ovos-react-loop` |
| `NativeToolCallEngine` | `ovos_agentic_loop/native_toolcall.py` | — (concrete) | — |
| `NativeToolCallEnginePlugin` | `ovos_agentic_loop/factory.py` | `opm.agents.chat` | `ovos-native-toolcall-loop` |
| `PlanAndExecuteEngine` | `ovos_agentic_loop/plan_execute.py:108` | — (concrete) | — |
| `PlanAndExecuteEnginePlugin` | `ovos_agentic_loop/factory.py:27` | `opm.agents.chat` | `ovos-plan-execute-loop` |
| `ReflexionEngine` | `ovos_agentic_loop/reflexion.py:82` | — (concrete) | — |
Expand Down
21 changes: 19 additions & 2 deletions docs/loop-architectures.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
| Use-case signal | Recommended loop |
| :--- | :--- |
| No tools, pure reasoning / arithmetic / logic | **Chain-of-Thought** |
| Single-turn tool use, general assistant | **ReAct** |
| Tool use with a brain that has native function-calling | **Native Tool-Call** |
| Single-turn tool use, any text brain (no native tools) | **ReAct** |
| Multi-step task with distinct, sequenced sub-goals | **Plan-and-Execute** |
| Correctness matters; agent may fail on first attempt | **Reflexion** |
| Multi-hop knowledge question (chain of facts) | **Self-Ask** |
Expand All @@ -20,7 +21,23 @@

The loops are not mutually exclusive. Reflexion *wraps* ReAct internally, so
it inherits every ReAct capability while adding the self-correction outer loop.
Plan-and-Execute uses its own mini-ReAct sub-loop per step.
Plan-and-Execute uses its own mini-ReAct sub-loop per step. **Native Tool-Call**
subclasses ReAct and falls back to it when the brain has no native function-calling.

---

## Native Tool-Call — provider-native function calling

**Entry point:** `ovos-native-toolcall-loop`
**Class:** `NativeToolCallEngine` — `ovos_agentic_loop/native_toolcall.py`
**Deep dive:** [native-toolcall-loop.md](native-toolcall-loop.md)

Hands the toolboxes to the brain via `continue_chat(tools=...)` and reads structured
`AgentMessage.tool_calls` back, executing each and feeding results as
`MessageRole.TOOL` messages. Requires a brain with `supports_tools = True` (e.g.
`ovos-openai-plugin`); otherwise it transparently falls back to the ReAct text loop.
Prefer this over ReAct whenever the brain supports native tool-calling — it is more
reliable and cheaper (the provider parses the calls, not a regex).

---

Expand Down
66 changes: 66 additions & 0 deletions docs/native-toolcall-loop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# NativeToolCallEngine — Deep Dive

## Overview

`NativeToolCallEngine` (`ovos_agentic_loop/native_toolcall.py`) drives a tool loop
using the brain's **native** function-calling instead of ReAct's text protocol. It
hands the toolboxes to the brain via `continue_chat(..., tools=...)` and reads back
**structured** `AgentMessage.tool_calls`, executes each through the existing
`ToolBox` machinery, and feeds the results back as `MessageRole.TOOL` messages until
the brain answers without requesting a tool.

It is a subclass of `ReActLoopEngine` (and therefore `AgenticLoopEngine` →
`ChatEngine`), reusing its brain wiring, toolbox loading, schema collection and tool
execution. Only the loop in `continue_chat` differs.

`NativeToolCallEnginePlugin` (`ovos_agentic_loop/factory.py`) is the OPM-registered
wrapper. Entry point: **`ovos-native-toolcall-loop`** (group `opm.agents.chat`).

## Native vs. ReAct — and the fallback

| | NativeToolCallEngine | ReActLoopEngine |
| --- | --- | --- |
| How the model calls a tool | provider-native `tool_calls` | `Action:`/`Action Input:` text, regex-parsed |
| Tool result fed back as | `MessageRole.TOOL` message | `Observation:` text in a user message |
| Brain requirement | `supports_tools = True` | any text ChatEngine |
| Reliability / token cost | higher / lower (provider parses) | lower / higher (prompt + parse) |

**Fallback:** if the configured brain does not advertise `supports_tools`,
`continue_chat` transparently delegates to the inherited ReAct text loop
(`super().continue_chat(...)`). So this engine works with **any** brain — it just
uses the better path when the brain supports it.

## The loop

1. **No brain** → return an error `AgentMessage`.
2. **Brain lacks `supports_tools`** → `return super().continue_chat(...)` (ReAct).
3. Otherwise iterate up to `max_iterations`:
- `resp = brain.continue_chat(loop_messages, tools=self.toolboxes)` — the
`ToolBox` objects are passed straight through; the brain normalizes them with
`ToolBox.normalize_tools` to its provider's tool format.
- If `resp.tool_calls` is empty → return `AgentMessage(ASSISTANT, resp.content)`.
- Else append the assistant turn (carrying `tool_calls`) **first**, then one
`MessageRole.TOOL` message per `ToolCall` (with `tool_call_id`/`name`),
executing via the inherited `_call_tool`. The assistant-before-tool ordering
is the invariant providers require.
4. **Max iterations** → one final tool-free `continue_chat(..., tools=None)` to
force a text answer.

## Config

| Key | Meaning | Default |
| --- | --- | --- |
| `brain` | ChatEngine plugin id used as the inner LLM (must set `supports_tools` to use native calls) | — |
| `toolboxes` | ToolBox plugin ids to load | — |
| `max_iterations` | Max tool-call cycles before forcing an answer | 10 |

## Streaming

Tool calls are driven through the non-streaming `continue_chat`. Token/sentence
streaming applies only to the terminal answer; structured `tool_calls` are not
streamed.

## Example

See [`examples/native_toolcall_persona.json`](../examples/native_toolcall_persona.json)
and [`examples/native_toolcall_persona.py`](../examples/native_toolcall_persona.py).
21 changes: 21 additions & 0 deletions examples/native_toolcall_persona.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"_comment": "Native tool-calling persona for ovos-persona. See native_toolcall_persona.py for the full example.",
"name": "Calculator",
"solvers": [
"ovos-native-toolcall-loop"
],
"ovos-native-toolcall-loop": {
"brain": "ovos-chat-openai-plugin",
"max_iterations": 5,
"system_prompt": "You are a precise assistant. Use the math tools to compute answers instead of guessing.",
"toolboxes": [
"ovos-math-tools"
],
"ovos-chat-openai-plugin": {
"_comment": "Needs a brain whose ChatEngine sets supports_tools=True (native function-calling). Point api_url at any OpenAI-compatible server with a tool-capable model.",
"model": "qwen2.5",
"api_url": "http://localhost:11434/v1/chat/completions",
"api_key": "not-needed-for-local"
}
}
}
86 changes: 86 additions & 0 deletions examples/native_toolcall_persona.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
#!/usr/bin/env python3
# Copyright 2025, OpenVoiceOS
#
# 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.
"""Native tool-calling demo — NativeToolCallEngine + MathToolBox + a local LLM.

Unlike the ReAct examples, this drives the brain's *native* function-calling: the
toolboxes are handed to the brain via ``continue_chat(tools=...)`` and structured
``tool_calls`` come back, so it needs a brain whose ChatEngine sets
``supports_tools = True`` (e.g. ``ovos-openai-plugin`` against a tool-capable model).
If the brain lacks native tool support, NativeToolCallEngine falls back to ReAct.

Run::

python examples/native_toolcall_persona.py

Prerequisites::

pip install ovos-agentic-loop ovos-openai-plugin
# a local OpenAI-compatible server with a tool-capable model, e.g.:
# ollama serve && ollama pull qwen2.5

Environment overrides: CALC_MODEL, CALC_API_URL.
"""
import os
import sys

_missing = []
try:
from ovos_agentic_loop.native_toolcall import NativeToolCallEngine
except ImportError:
_missing.append("ovos-agentic-loop")
try:
from ovos_openai_plugin.chat import OpenAIChatEngine # type: ignore[import-untyped]
except ImportError:
_missing.append("ovos-openai-plugin")
try:
from ovos_agentic_loop.tools.math import MathToolBox
except ImportError:
_missing.append("ovos-agentic-loop[math]")

if _missing:
sys.exit(f"Missing dependencies: {', '.join(_missing)}")


def main() -> None:
brain = OpenAIChatEngine({
"model": os.environ.get("CALC_MODEL", "qwen2.5"),
"api_url": os.environ.get("CALC_API_URL",
"http://localhost:11434/v1/chat/completions"),
"api_key": "not-needed-for-local",
"system_prompt": "You are a precise assistant. Use the math tools to "
"compute answers instead of guessing.",
})
if not getattr(brain, "supports_tools", False):
print("NOTE: this brain reports no native tool support — the engine will "
"fall back to the ReAct text loop.")

engine = NativeToolCallEngine({"max_iterations": 5})
engine.set_brain(brain)
engine.load_toolboxes([MathToolBox()])

print("Calculator agent — ask a math question (Ctrl-C to quit).\n")
try:
while True:
query = input("you> ").strip()
if not query:
continue
answer = engine.get_response(query)
print(f"bot> {answer}\n")
except (KeyboardInterrupt, EOFError):
print("\nbye!")


if __name__ == "__main__":
main()
14 changes: 14 additions & 0 deletions ovos_agentic_loop/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"""OPM entry-point factory classes for ovos-agentic-loop."""
from ovos_agentic_loop.chain_of_thought import ChainOfThoughtEngine
from ovos_agentic_loop.critic import CRITICEngine
from ovos_agentic_loop.native_toolcall import NativeToolCallEngine
from ovos_agentic_loop.plan_execute import PlanAndExecuteEngine
from ovos_agentic_loop.react import ReActLoopEngine
from ovos_agentic_loop.reflexion import ReflexionEngine
Expand All @@ -21,6 +22,7 @@

__all__ = [
"ReActLoopEnginePlugin",
"NativeToolCallEnginePlugin",
"PlanAndExecuteEnginePlugin",
"ReflexionEnginePlugin",
"SelfAskEnginePlugin",
Expand All @@ -30,6 +32,18 @@
]


class NativeToolCallEnginePlugin(NativeToolCallEngine):
"""
OPM-registered plugin class for the native tool-calling loop engine.

Uses the brain's native ``tool_calls`` when it advertises ``supports_tools``,
falling back to the ReAct text loop otherwise.

Entry point group: ``opm.agents.chat``
Entry point name: ``ovos-native-toolcall-loop``
"""


class ReActLoopEnginePlugin(ReActLoopEngine):
"""
OPM-registered plugin class for the ReAct loop engine.
Expand Down
Loading