feat(astrolabe): add admin search settings and enhanced UI
Add comprehensive admin controls for the unified search provider and enhance the frontend UI with filtering and visualization improvements. **Admin Settings:** - Configure default search algorithm (hybrid, semantic, bm25) - Set fusion method for hybrid search (rrf, dbsf) - Adjust minimum score threshold (0-100%) - Set result limit (1-100 results) **Frontend Enhancements:** - Add score-based result filtering with slider control - Add expandable excerpts for search results - Improve result visualization and formatting - Add algorithm badge to show search method used **API Changes:** - Add `/api/admin/search-settings` POST endpoint - Add `searchForUnifiedSearch()` method to McpServerClient - Load and apply admin settings in SemanticSearchProvider **Technical Details:** - Settings stored in app config table - Defaults: hybrid algorithm, rrf fusion, 0% threshold, 20 results - SemanticSearchProvider respects admin-configured limits 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.5
parent
24e63a967a
commit
4cce4f6392
+85
-1
@@ -6,12 +6,14 @@ namespace OCA\Astroglobe\Controller;
|
||||
|
||||
use OCA\Astroglobe\Service\McpServerClient;
|
||||
use OCA\Astroglobe\Service\McpTokenStorage;
|
||||
use OCA\Astroglobe\Settings\Admin as AdminSettings;
|
||||
use OCP\AppFramework\Controller;
|
||||
use OCP\AppFramework\Http;
|
||||
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
|
||||
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
|
||||
use OCP\AppFramework\Http\JSONResponse;
|
||||
use OCP\AppFramework\Http\RedirectResponse;
|
||||
use OCP\IConfig;
|
||||
use OCP\IRequest;
|
||||
use OCP\IURLGenerator;
|
||||
use OCP\IUserSession;
|
||||
@@ -28,6 +30,7 @@ class ApiController extends Controller {
|
||||
private $urlGenerator;
|
||||
private $logger;
|
||||
private $tokenStorage;
|
||||
private $config;
|
||||
|
||||
public function __construct(
|
||||
string $appName,
|
||||
@@ -36,7 +39,8 @@ class ApiController extends Controller {
|
||||
IUserSession $userSession,
|
||||
IURLGenerator $urlGenerator,
|
||||
LoggerInterface $logger,
|
||||
McpTokenStorage $tokenStorage
|
||||
McpTokenStorage $tokenStorage,
|
||||
IConfig $config
|
||||
) {
|
||||
parent::__construct($appName, $request);
|
||||
$this->client = $client;
|
||||
@@ -44,6 +48,7 @@ class ApiController extends Controller {
|
||||
$this->urlGenerator = $urlGenerator;
|
||||
$this->logger = $logger;
|
||||
$this->tokenStorage = $tokenStorage;
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -223,4 +228,83 @@ class ApiController extends Controller {
|
||||
'status' => $status
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save admin search settings.
|
||||
*
|
||||
* Admin-only endpoint to configure AI Search provider parameters.
|
||||
*
|
||||
* @return JSONResponse
|
||||
*/
|
||||
public function saveSearchSettings(): JSONResponse {
|
||||
// Parse JSON body
|
||||
$input = file_get_contents('php://input');
|
||||
$data = json_decode($input, true);
|
||||
|
||||
if ($data === null) {
|
||||
return new JSONResponse([
|
||||
'success' => false,
|
||||
'error' => 'Invalid JSON body'
|
||||
], Http::STATUS_BAD_REQUEST);
|
||||
}
|
||||
|
||||
// Validate and save algorithm
|
||||
$validAlgorithms = ['hybrid', 'semantic', 'bm25'];
|
||||
$algorithm = $data['algorithm'] ?? AdminSettings::DEFAULT_SEARCH_ALGORITHM;
|
||||
if (!in_array($algorithm, $validAlgorithms)) {
|
||||
$algorithm = AdminSettings::DEFAULT_SEARCH_ALGORITHM;
|
||||
}
|
||||
$this->config->setAppValue(
|
||||
$this->appName,
|
||||
AdminSettings::SETTING_SEARCH_ALGORITHM,
|
||||
$algorithm
|
||||
);
|
||||
|
||||
// Validate and save fusion method
|
||||
$validFusions = ['rrf', 'dbsf'];
|
||||
$fusion = $data['fusion'] ?? AdminSettings::DEFAULT_SEARCH_FUSION;
|
||||
if (!in_array($fusion, $validFusions)) {
|
||||
$fusion = AdminSettings::DEFAULT_SEARCH_FUSION;
|
||||
}
|
||||
$this->config->setAppValue(
|
||||
$this->appName,
|
||||
AdminSettings::SETTING_SEARCH_FUSION,
|
||||
$fusion
|
||||
);
|
||||
|
||||
// Validate and save score threshold (0-100)
|
||||
$scoreThreshold = (int)($data['scoreThreshold'] ?? AdminSettings::DEFAULT_SEARCH_SCORE_THRESHOLD);
|
||||
$scoreThreshold = max(0, min(100, $scoreThreshold));
|
||||
$this->config->setAppValue(
|
||||
$this->appName,
|
||||
AdminSettings::SETTING_SEARCH_SCORE_THRESHOLD,
|
||||
(string)$scoreThreshold
|
||||
);
|
||||
|
||||
// Validate and save limit (5-100)
|
||||
$limit = (int)($data['limit'] ?? AdminSettings::DEFAULT_SEARCH_LIMIT);
|
||||
$limit = max(5, min(100, $limit));
|
||||
$this->config->setAppValue(
|
||||
$this->appName,
|
||||
AdminSettings::SETTING_SEARCH_LIMIT,
|
||||
(string)$limit
|
||||
);
|
||||
|
||||
$this->logger->info('Admin search settings saved', [
|
||||
'algorithm' => $algorithm,
|
||||
'fusion' => $fusion,
|
||||
'scoreThreshold' => $scoreThreshold,
|
||||
'limit' => $limit,
|
||||
]);
|
||||
|
||||
return new JSONResponse([
|
||||
'success' => true,
|
||||
'settings' => [
|
||||
'algorithm' => $algorithm,
|
||||
'fusion' => $fusion,
|
||||
'scoreThreshold' => $scoreThreshold,
|
||||
'limit' => $limit,
|
||||
]
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -367,6 +367,10 @@ class OAuthController extends Controller {
|
||||
'astroglobe.oauth.oauthCallback'
|
||||
);
|
||||
|
||||
// Get public MCP server URL for token audience (RFC 8707 Resource Indicator)
|
||||
// Use public URL that clients/browsers see, not internal Docker URL
|
||||
$mcpServerPublicUrl = $this->config->getSystemValue('mcp_server_public_url', $mcpServerUrl);
|
||||
|
||||
// Build authorization URL with PKCE
|
||||
$params = [
|
||||
'client_id' => 'nextcloudMcpServerUIPublicClient', // Public client ID (32+ chars required by NC OIDC)
|
||||
@@ -376,6 +380,7 @@ class OAuthController extends Controller {
|
||||
'state' => $state,
|
||||
'code_challenge' => $codeChallenge,
|
||||
'code_challenge_method' => 'S256',
|
||||
'resource' => $mcpServerPublicUrl, // RFC 8707 Resource Indicator - request token with MCP server audience
|
||||
];
|
||||
|
||||
return $authEndpoint . '?' . http_build_query($params);
|
||||
|
||||
@@ -50,7 +50,7 @@ class SemanticSearchProvider implements IProvider {
|
||||
* Display name shown in search results grouping.
|
||||
*/
|
||||
public function getName(): string {
|
||||
return $this->l10n->t('Astroglob');
|
||||
return $this->l10n->t('Astroglobe');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -248,6 +248,80 @@ class McpServerClient {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute semantic search for Nextcloud Unified Search.
|
||||
*
|
||||
* Simplified search method specifically for the unified search provider.
|
||||
* Uses OAuth bearer token for authentication and user-scoped filtering.
|
||||
*
|
||||
* @param string $query Search query string
|
||||
* @param string $token OAuth bearer token for authentication
|
||||
* @param int $limit Maximum number of results (default: 20)
|
||||
* @param int $offset Pagination offset (default: 0)
|
||||
* @param string $algorithm Search algorithm: hybrid, semantic, or bm25 (default: hybrid)
|
||||
* @param string $fusion Fusion method for hybrid: rrf or dbsf (default: rrf)
|
||||
* @param float $scoreThreshold Minimum score threshold 0-1 (default: 0)
|
||||
* @return array{
|
||||
* results?: array<array{
|
||||
* id?: string|int,
|
||||
* title?: string,
|
||||
* doc_type?: string,
|
||||
* excerpt?: string,
|
||||
* score?: float,
|
||||
* path?: string,
|
||||
* board_id?: int,
|
||||
* card_id?: int
|
||||
* }>,
|
||||
* total_found?: int,
|
||||
* algorithm_used?: string,
|
||||
* error?: string
|
||||
* }
|
||||
*/
|
||||
public function searchForUnifiedSearch(
|
||||
string $query,
|
||||
string $token,
|
||||
int $limit = 20,
|
||||
int $offset = 0,
|
||||
string $algorithm = 'hybrid',
|
||||
string $fusion = 'rrf',
|
||||
float $scoreThreshold = 0.0
|
||||
): array {
|
||||
try {
|
||||
$response = $this->httpClient->post(
|
||||
$this->baseUrl . '/api/v1/search',
|
||||
[
|
||||
'headers' => [
|
||||
'Authorization' => 'Bearer ' . $token,
|
||||
'Content-Type' => 'application/json',
|
||||
],
|
||||
'json' => [
|
||||
'query' => $query,
|
||||
'algorithm' => $algorithm,
|
||||
'fusion' => $fusion,
|
||||
'score_threshold' => $scoreThreshold,
|
||||
'limit' => min($limit, 100),
|
||||
'offset' => $offset,
|
||||
'include_pca' => false,
|
||||
'include_chunks' => true,
|
||||
]
|
||||
]
|
||||
);
|
||||
$data = json_decode($response->getBody(), true);
|
||||
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
throw new \RuntimeException('Invalid JSON response from server');
|
||||
}
|
||||
|
||||
return $data;
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->error('Unified search failed', [
|
||||
'error' => $e->getMessage(),
|
||||
'query' => $query,
|
||||
]);
|
||||
return ['error' => $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the MCP server is reachable and API key is valid.
|
||||
*
|
||||
|
||||
+37
@@ -18,6 +18,17 @@ use OCP\Settings\ISettings;
|
||||
* configuration, and provides administrative controls.
|
||||
*/
|
||||
class Admin implements ISettings {
|
||||
// Search settings keys and defaults
|
||||
public const SETTING_SEARCH_ALGORITHM = 'search_algorithm';
|
||||
public const SETTING_SEARCH_FUSION = 'search_fusion';
|
||||
public const SETTING_SEARCH_SCORE_THRESHOLD = 'search_score_threshold';
|
||||
public const SETTING_SEARCH_LIMIT = 'search_limit';
|
||||
|
||||
public const DEFAULT_SEARCH_ALGORITHM = 'hybrid';
|
||||
public const DEFAULT_SEARCH_FUSION = 'rrf';
|
||||
public const DEFAULT_SEARCH_SCORE_THRESHOLD = 0;
|
||||
public const DEFAULT_SEARCH_LIMIT = 20;
|
||||
|
||||
private $client;
|
||||
private $config;
|
||||
private $initialState;
|
||||
@@ -59,6 +70,30 @@ class Admin implements ISettings {
|
||||
);
|
||||
}
|
||||
|
||||
// Load search settings from app config
|
||||
$searchSettings = [
|
||||
'algorithm' => $this->config->getAppValue(
|
||||
Application::APP_ID,
|
||||
self::SETTING_SEARCH_ALGORITHM,
|
||||
self::DEFAULT_SEARCH_ALGORITHM
|
||||
),
|
||||
'fusion' => $this->config->getAppValue(
|
||||
Application::APP_ID,
|
||||
self::SETTING_SEARCH_FUSION,
|
||||
self::DEFAULT_SEARCH_FUSION
|
||||
),
|
||||
'scoreThreshold' => (int)$this->config->getAppValue(
|
||||
Application::APP_ID,
|
||||
self::SETTING_SEARCH_SCORE_THRESHOLD,
|
||||
(string)self::DEFAULT_SEARCH_SCORE_THRESHOLD
|
||||
),
|
||||
'limit' => (int)$this->config->getAppValue(
|
||||
Application::APP_ID,
|
||||
self::SETTING_SEARCH_LIMIT,
|
||||
(string)self::DEFAULT_SEARCH_LIMIT
|
||||
),
|
||||
];
|
||||
|
||||
// Provide initial state for Vue.js frontend (if needed)
|
||||
$this->initialState->provideInitialState('server-data', [
|
||||
'serverStatus' => $serverStatus,
|
||||
@@ -67,6 +102,7 @@ class Admin implements ISettings {
|
||||
'serverUrl' => $serverUrl,
|
||||
'apiKeyConfigured' => $apiKeyConfigured,
|
||||
],
|
||||
'searchSettings' => $searchSettings,
|
||||
]);
|
||||
|
||||
$parameters = [
|
||||
@@ -75,6 +111,7 @@ class Admin implements ISettings {
|
||||
'serverUrl' => $serverUrl,
|
||||
'apiKeyConfigured' => $apiKeyConfigured,
|
||||
'vectorSyncEnabled' => $serverStatus['vector_sync_enabled'] ?? false,
|
||||
'searchSettings' => $searchSettings,
|
||||
];
|
||||
|
||||
return new TemplateResponse(
|
||||
|
||||
+1
-1
@@ -47,6 +47,6 @@ class AdminSection implements IIconSection {
|
||||
* @return string Section icon (SVG or image URL)
|
||||
*/
|
||||
public function getIcon(): string {
|
||||
return $this->urlGenerator->imagePath('astroglobe', 'app.svg');
|
||||
return $this->urlGenerator->imagePath('astroglobe', 'app-dark.svg');
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -47,6 +47,6 @@ class PersonalSection implements IIconSection {
|
||||
* @return string Section icon (SVG or image URL)
|
||||
*/
|
||||
public function getIcon(): string {
|
||||
return $this->urlGenerator->imagePath('astroglobe', 'app.svg');
|
||||
return $this->urlGenerator->imagePath('astroglobe', 'app-dark.svg');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user