Skip to content
Merged
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
6 changes: 6 additions & 0 deletions appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -548,6 +548,12 @@
['name' => 'chat#clearHistory', 'url' => '/api/chat/history', 'verb' => 'DELETE'],
['name' => 'chat#getChatStats', 'url' => '/api/chat/stats', 'verb' => 'GET'],
['name' => 'chat#sendFeedback', 'url' => '/api/conversations/{conversationUuid}/messages/{messageId}/feedback', 'verb' => 'POST', 'requirements' => ['conversationUuid' => '[^/]+', 'messageId' => '\\d+']],

// Chat - Health probe (PublicPage — no auth required).
['name' => 'chatHealth#health', 'url' => '/api/chat/health', 'verb' => 'GET'],

// Chat - SSE streaming endpoint (authenticated).
['name' => 'chatStream#stream', 'url' => '/api/chat/stream', 'verb' => 'POST'],

// Conversations - AI Conversation management.
['name' => 'conversation#index', 'url' => '/api/conversations', 'verb' => 'GET'],
Expand Down
38 changes: 38 additions & 0 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,11 @@
use OCA\OpenRegister\Service\LanguageService;
use OCA\OpenRegister\Middleware\LanguageMiddleware;
use OCA\OpenRegister\Capabilities\UrnCapability;
use OCA\OpenRegister\Mcp\IMcpToolProvider;
use OCA\OpenRegister\Mcp\BuiltIn\RegistersToolProvider;
use OCA\OpenRegister\Mcp\BuiltIn\SchemasToolProvider;
use OCA\OpenRegister\Mcp\BuiltIn\ObjectsToolProvider;
use OCA\OpenRegister\Service\Mcp\McpToolsService;

/**
* Class Application
Expand Down Expand Up @@ -301,6 +306,7 @@ function () {
$this->registerVectorizationService(context: $context);
$this->registerObjectInteractionServices(context: $context);
$this->registerEventListeners(context: $context);
$this->registerMcpToolProviders(context: $context);

// Register the annotation-driven INotifier so notifications fired by
// AnnotationNotificationDispatcher get a parsed subject — without
Expand Down Expand Up @@ -915,6 +921,38 @@ private function registerEventListeners(IRegistrationContext $context): void
$context->registerEventListener(SchemaDeletedEvent::class, $activityListener);
}//end registerEventListeners()

/**
* Register MCP tool providers (built-ins first).
*
* Wires the three built-in IMcpToolProvider implementations into
* McpToolsService. External apps may call addProvider() after boot
* or override the McpToolsService binding to prepend their own providers.
*
* @param IRegistrationContext $context The registration context
*
* @return void
*
* @spec openspec/changes/ai-chat-companion-orchestrator/specs/chat-ai/spec.md#mcptoolsservice-provider-discovery-refactor
*/
private function registerMcpToolProviders(IRegistrationContext $context): void
{
$context->registerService(
McpToolsService::class,
function (ContainerInterface $container) {
$providers = [
$container->get(RegistersToolProvider::class),
$container->get(SchemasToolProvider::class),
$container->get(ObjectsToolProvider::class),
];

return new McpToolsService(
providers: $providers,
logger: $container->get('Psr\Log\LoggerInterface')
);
}
);
}//end registerMcpToolProviders()

/**
* Boot application components
*
Expand Down
121 changes: 121 additions & 0 deletions lib/Controller/ChatHealthController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
<?php

/**
* OpenRegister Chat Health Controller
*
* Lightweight health probe for the AI chat backend. Allows the
* nextcloud-vue AI companion widget to detect at mount time whether
* the chat backend is configured and reachable — without requiring a
* Nextcloud session (PublicPage).
*
* @category Controller
* @package OCA\OpenRegister\Controller
*
* @author Conduction Development Team <dev@conduction.nl>
* @copyright 2026 Conduction BV
* @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
*
* @version GIT: <git_id>
*
* @link https://OpenRegister.app
*
* @spec openspec/changes/ai-chat-companion-orchestrator/specs/chat-ai/spec.md#health-probe-endpoint-get-apichathealth
*/

declare(strict_types=1);

namespace OCA\OpenRegister\Controller;

use OCA\OpenRegister\Service\SettingsService;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
use OCP\AppFramework\Http\Attribute\PublicPage;
use OCP\AppFramework\Http\JSONResponse;
use OCP\IRequest;

/**
* ChatHealthController
*
* Provides GET /api/chat/health — an unauthenticated endpoint that the
* nextcloud-vue widget probes once at mount time to decide whether to
* render the AI companion button.
*
* Returns HTTP 200 + {status:"ok",capabilities:["chat","stream"]} when at
* least one LLM provider is configured; HTTP 503 + {status:"no_provider"}
* otherwise.
*
* @category Controller
* @package OCA\OpenRegister\Controller
*
* @psalm-suppress UnusedClass
*/
class ChatHealthController extends Controller
{

/**
* Settings service for LLM configuration lookup.
*
* @var SettingsService
*/
private readonly SettingsService $settingsService;

/**
* Constructor
*
* @param string $appName Application name
* @param IRequest $request HTTP request object
* @param SettingsService $settingsService Settings service for LLM config
*
* @return void
*/
public function __construct(
string $appName,
IRequest $request,
SettingsService $settingsService
) {
parent::__construct(appName: $appName, request: $request);
$this->settingsService = $settingsService;
}//end __construct()

/**
* Health probe for the AI chat backend
*
* Returns 200 when a chat provider is configured, 503 otherwise.
* Annotated as PublicPage so the widget can probe without authentication.
*
* @PublicPage
*
* @NoCSRFRequired
*
* @return JSONResponse 200 or 503 JSON response
*/
#[PublicPage]
#[NoCSRFRequired]
public function health(): JSONResponse
{
try {
$llmConfig = $this->settingsService->getLLMSettingsOnly();
$chatProvider = $llmConfig['chatProvider'] ?? null;

if (empty($chatProvider) === true) {
return new JSONResponse(
data: ['status' => 'no_provider'],
statusCode: 503
);
}

return new JSONResponse(
data: [
'status' => 'ok',
'capabilities' => ['chat', 'stream'],
],
statusCode: 200
);
} catch (\Throwable $e) {
return new JSONResponse(
data: ['status' => 'no_provider'],
statusCode: 503
);
}//end try
}//end health()
}//end class
Loading
Loading