Talk AI is a multi-bot AI assistant manager for Nextcloud Talk: per-bot prompts and models, agentic tool calling (MCP + built-in tools), RAG over Nextcloud files, room-document search, vision and speech-to-text attachments, persistent bot wikis, approval workflows, rate limiting, and multi-provider LLM support (any OpenAI-compatible endpoint). Developed within EDUC - the European Digital UniverCity (https://educalliance.eu), where it runs as the 'EDUC AI' assistant on the alliance-wide Nextcloud portal. This public repository is the upstream point of truth; deployment-specific tools plug in via the tool-provider extension point (docs/TOOL_PROVIDERS.md). License: AGPL-3.0-or-later.
3.5 KiB
Tool-Provider Extension Point
Other Nextcloud apps can contribute additional agent tools to Talk AI bots without any changes to this app. Contributed tools behave exactly like the core built-in tools: they share the name-based per-bot loadout storage, the per-bot enable/disable UI, execution policies and the agent-loop dispatch.
This is the mechanism used to keep deployment-specific tools out of the generic core: a companion app can ship its own tools (e.g. a course-catalogue search) that plug into every bot without forking this app.
How it works
- The core app exposes
OCA\EducAI\ToolProvider\ToolProviderRegistry, which all consumers (agent executor, tools API, bot service) use to list and execute name-based tools. - On first use per request, the registry dispatches
OCA\EducAI\ToolProvider\CollectToolProvidersEvent. - Listeners register implementations of
OCA\EducAI\ToolProvider\IToolProvideron the event.
Registering a provider from another app
In your app's Application::register():
$context->registerEventListener(
\OCA\EducAI\ToolProvider\CollectToolProvidersEvent::class,
\OCA\MyApp\Listener\MyToolProviderListener::class
);
The listener:
class MyToolProviderListener implements IEventListener {
public function __construct(private MyToolProvider $provider) {
}
public function handle(Event $event): void {
if (!$event instanceof CollectToolProvidersEvent) {
return;
}
$event->registerProvider($this->provider);
}
}
Implementing IToolProvider
| Method | Purpose |
|---|---|
getTools() |
Currently available tool definitions (may be [] when unconfigured). Called when assembling a bot's loadout and for the tools UI. |
providesTool($name) |
Whether this provider executes the given tool name (include legacy aliases). |
executeTool($name, $arguments, $config) |
Run the tool; return {content: [{type, text}], isError} (MCP-style result). |
getToolMetadata() |
Static UI labels/summaries per tool name — must work even when the provider is disabled, so assigned tools still render properly in the UI. |
setInvocationContext($context) |
Receives bot id / room token / attachments before execution; no-op for stateless providers. |
Tool definition shape (same as core built-ins):
[
'name' => 'my_tool', // unique, snake_case
'description' => 'LLM-facing description',
'schema' => [ /* JSON schema */ ],
'policy' => $policyService->searchToolPolicy(), // or readToolPolicy()
'label' => 'My Tool', // optional UI label
'summary' => 'One-line UI summary', // optional UI description
]
Execution policies drive loop budgets and the forced-search heuristics; use
ToolExecutionPolicyService::searchToolPolicy() / readToolPolicy() for
read-only tools. Mutating provider tools should not be marked read-only —
construct a stricter policy accordingly.
Guarantees & caveats
- Name collisions: core tools can never be shadowed. A provider tool whose name collides with an existing tool is skipped with a warning in the log.
- Failure isolation: a provider that throws in
getTools()orsetInvocationContext()is logged and skipped; it cannot break the loadout. - Storage: assignments are stored by tool name in the existing
bot_toolstable (is_built_inpath) — no schema changes needed for provider tools, and removing a provider app leaves stale assignments inert (they simply stop matching any definition).