|
@@ -0,0 +1,1594 @@
|
|
|
|
|
+#include "smartbotic/webserver/tool_service.hpp"
|
|
|
|
|
+
|
|
|
|
|
+#include <spdlog/spdlog.h>
|
|
|
|
|
+
|
|
|
|
|
+#include "smartbotic/webserver/collection_service.hpp"
|
|
|
|
|
+#include "smartbotic/webserver/document_service.hpp"
|
|
|
|
|
+#include "smartbotic/webserver/page_service.hpp"
|
|
|
|
|
+#include "smartbotic/webserver/permissions.hpp"
|
|
|
|
|
+#include "smartbotic/webserver/view_service.hpp"
|
|
|
|
|
+#include "smartbotic/webserver/workspace_service.hpp"
|
|
|
|
|
+
|
|
|
|
|
+namespace smartbotic::webserver {
|
|
|
|
|
+
|
|
|
|
|
+// Helper to build JSON schema strings
|
|
|
|
|
+namespace {
|
|
|
|
|
+
|
|
|
|
|
+std::string MakeObjectSchema(const nlohmann::json& properties,
|
|
|
|
|
+ const std::vector<std::string>& required = {}) {
|
|
|
|
|
+ nlohmann::json schema;
|
|
|
|
|
+ schema["type"] = "object";
|
|
|
|
|
+ schema["properties"] = properties;
|
|
|
|
|
+ if (!required.empty()) {
|
|
|
|
|
+ schema["required"] = required;
|
|
|
|
|
+ }
|
|
|
|
|
+ return schema.dump();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+} // namespace
|
|
|
|
|
+
|
|
|
|
|
+ToolService::ToolService(AuthorizationService& authorization_service,
|
|
|
|
|
+ WorkspaceService& workspace_service,
|
|
|
|
|
+ ViewService& view_service,
|
|
|
|
|
+ PageService& page_service,
|
|
|
|
|
+ DocumentService& document_service,
|
|
|
|
|
+ CollectionService& collection_service)
|
|
|
|
|
+ : authorization_service_(authorization_service),
|
|
|
|
|
+ workspace_service_(workspace_service),
|
|
|
|
|
+ view_service_(view_service),
|
|
|
|
|
+ page_service_(page_service),
|
|
|
|
|
+ document_service_(document_service),
|
|
|
|
|
+ collection_service_(collection_service) {}
|
|
|
|
|
+
|
|
|
|
|
+ToolService::~ToolService() = default;
|
|
|
|
|
+
|
|
|
|
|
+void ToolService::Initialize() {
|
|
|
|
|
+ spdlog::info("Initializing Tool Service...");
|
|
|
|
|
+
|
|
|
|
|
+ RegisterNavigationTools();
|
|
|
|
|
+ RegisterWorkspaceTools();
|
|
|
|
|
+ RegisterViewTools();
|
|
|
|
|
+ RegisterPageTools();
|
|
|
|
|
+ RegisterDocumentTools();
|
|
|
|
|
+
|
|
|
|
|
+ spdlog::info("Tool Service initialized with {} tools", tools_.size());
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void ToolService::RegisterTool(const CrmToolDefinition& definition, CrmToolExecutor executor) {
|
|
|
|
|
+ tools_[definition.name] = definition;
|
|
|
|
|
+ executors_[definition.name] = std::move(executor);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void ToolService::RegisterTool(const CrmToolDefinition& definition) {
|
|
|
|
|
+ tools_[definition.name] = definition;
|
|
|
|
|
+ // No executor - navigation tools are handled client-side
|
|
|
|
|
+ navigation_tools_.push_back(definition.name);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+auto ToolService::GetAvailableTools(const AuthUser& user, const std::string& workspace_id)
|
|
|
|
|
+ -> std::vector<CrmToolDefinition> {
|
|
|
|
|
+ std::vector<CrmToolDefinition> available;
|
|
|
|
|
+
|
|
|
|
|
+ for (const auto& [name, tool] : tools_) {
|
|
|
|
|
+ if (HasToolPermissions(user, tool, workspace_id)) {
|
|
|
|
|
+ available.push_back(tool);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return available;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+auto ToolService::GetTool(const std::string& name) -> std::optional<CrmToolDefinition> {
|
|
|
|
|
+ auto it = tools_.find(name);
|
|
|
|
|
+ if (it != tools_.end()) {
|
|
|
|
|
+ return it->second;
|
|
|
|
|
+ }
|
|
|
|
|
+ return std::nullopt;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+auto ToolService::CanUseTool(const AuthUser& user, const std::string& tool_name,
|
|
|
|
|
+ const std::string& workspace_id) -> bool {
|
|
|
|
|
+ auto tool = GetTool(tool_name);
|
|
|
|
|
+ if (!tool) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ return HasToolPermissions(user, *tool, workspace_id);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+auto ToolService::ExecuteTool(const std::string& tool_name,
|
|
|
|
|
+ const nlohmann::json& arguments,
|
|
|
|
|
+ const ToolExecutionContext& context) -> ToolExecutionResult {
|
|
|
|
|
+ // Find the tool
|
|
|
|
|
+ auto tool = GetTool(tool_name);
|
|
|
|
|
+ if (!tool) {
|
|
|
|
|
+ return {false, "Tool not found: " + tool_name, true, std::nullopt};
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Re-validate permissions at execution time
|
|
|
|
|
+ if (!HasToolPermissions(context.user, *tool, context.workspace_id)) {
|
|
|
|
|
+ return {false, "Permission denied for tool: " + tool_name, true, std::nullopt};
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Check workspace context requirement
|
|
|
|
|
+ if (tool->permissions.requires_workspace_context && context.workspace_id.empty()) {
|
|
|
|
|
+ return {false, "Tool requires workspace context: " + tool_name, true, std::nullopt};
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Find and execute the executor
|
|
|
|
|
+ auto executor_it = executors_.find(tool_name);
|
|
|
|
|
+ if (executor_it == executors_.end()) {
|
|
|
|
|
+ // Navigation tools don't have executors
|
|
|
|
|
+ if (IsNavigationTool(tool_name)) {
|
|
|
|
|
+ return {false, "Navigation tools must be executed client-side", true, std::nullopt};
|
|
|
|
|
+ }
|
|
|
|
|
+ return {false, "No executor registered for tool: " + tool_name, true, std::nullopt};
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ return executor_it->second(arguments, context);
|
|
|
|
|
+ } catch (const std::exception& e) {
|
|
|
|
|
+ spdlog::error("Tool execution failed for {}: {}", tool_name, e.what());
|
|
|
|
|
+ return {false, std::string("Tool execution error: ") + e.what(), true, std::nullopt};
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+auto ToolService::IsNavigationTool(const std::string& tool_name) -> bool {
|
|
|
|
|
+ return std::find(navigation_tools_.begin(), navigation_tools_.end(), tool_name) !=
|
|
|
|
|
+ navigation_tools_.end();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+auto ToolService::ToLlmToolsJson(const std::vector<CrmToolDefinition>& tools) -> nlohmann::json {
|
|
|
|
|
+ nlohmann::json result = nlohmann::json::array();
|
|
|
|
|
+
|
|
|
|
|
+ for (const auto& tool : tools) {
|
|
|
|
|
+ nlohmann::json tool_json;
|
|
|
|
|
+ tool_json["name"] = tool.name;
|
|
|
|
|
+ tool_json["description"] = tool.description;
|
|
|
|
|
+
|
|
|
|
|
+ // Parse input_schema string to JSON
|
|
|
|
|
+ try {
|
|
|
|
|
+ tool_json["input_schema"] = nlohmann::json::parse(tool.input_schema);
|
|
|
|
|
+ } catch (...) {
|
|
|
|
|
+ tool_json["input_schema"] = nlohmann::json::object();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ result.push_back(tool_json);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return result;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+auto ToolService::HasToolPermissions(const AuthUser& user,
|
|
|
|
|
+ const CrmToolDefinition& tool,
|
|
|
|
|
+ const std::string& workspace_id) -> bool {
|
|
|
|
|
+ // Check if user is superadmin (can use any tool)
|
|
|
|
|
+ if (authorization_service_.IsSuperadmin(user)) {
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Check workspace context requirement
|
|
|
|
|
+ if (tool.permissions.requires_workspace_context && workspace_id.empty()) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Check any_of permissions (need at least one)
|
|
|
|
|
+ if (!tool.permissions.any_of.empty()) {
|
|
|
|
|
+ bool has_any = false;
|
|
|
|
|
+ for (const auto& perm : tool.permissions.any_of) {
|
|
|
|
|
+ // Replace * with workspace_id if present
|
|
|
|
|
+ std::string resolved_perm = perm;
|
|
|
|
|
+ if (!workspace_id.empty()) {
|
|
|
|
|
+ size_t pos = resolved_perm.find(":*:");
|
|
|
|
|
+ while (pos != std::string::npos) {
|
|
|
|
|
+ resolved_perm.replace(pos, 3, ":" + workspace_id + ":");
|
|
|
|
|
+ pos = resolved_perm.find(":*:", pos + workspace_id.length() + 2);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (authorization_service_.HasPermission(user, resolved_perm)) {
|
|
|
|
|
+ has_any = true;
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!has_any) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Check all_of permissions (need all)
|
|
|
|
|
+ if (!tool.permissions.all_of.empty()) {
|
|
|
|
|
+ for (const auto& perm : tool.permissions.all_of) {
|
|
|
|
|
+ std::string resolved_perm = perm;
|
|
|
|
|
+ if (!workspace_id.empty()) {
|
|
|
|
|
+ size_t pos = resolved_perm.find(":*:");
|
|
|
|
|
+ while (pos != std::string::npos) {
|
|
|
|
|
+ resolved_perm.replace(pos, 3, ":" + workspace_id + ":");
|
|
|
|
|
+ pos = resolved_perm.find(":*:", pos + workspace_id.length() + 2);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!authorization_service_.HasPermission(user, resolved_perm)) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return true;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// =============================================================================
|
|
|
|
|
+// Navigation Tools Registration
|
|
|
|
|
+// =============================================================================
|
|
|
|
|
+
|
|
|
|
|
+void ToolService::RegisterNavigationTools() {
|
|
|
|
|
+ // navigate_to_page
|
|
|
|
|
+ {
|
|
|
|
|
+ nlohmann::json props;
|
|
|
|
|
+ props["page_slug"] = {{"type", "string"}, {"description", "Page slug (URL-friendly name, e.g., 'dashboard', 'reports')"}};
|
|
|
|
|
+ props["page_id"] = {{"type", "string"}, {"description", "Alternative: Page ID if slug is unknown"}};
|
|
|
|
|
+
|
|
|
|
|
+ RegisterTool({
|
|
|
|
|
+ .name = "navigate_to_page",
|
|
|
|
|
+ .description = "Navigate the user to a custom page in the current workspace. Use this when the user wants to see a specific dashboard or page.",
|
|
|
|
|
+ .input_schema = MakeObjectSchema(props),
|
|
|
|
|
+ .category = "navigation",
|
|
|
|
|
+ .permissions = {
|
|
|
|
|
+ .any_of = {std::string(permissions::kPageReadAll), std::string(permissions::kPageReadOwn)},
|
|
|
|
|
+ .all_of = {},
|
|
|
|
|
+ .requires_workspace_context = true
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // navigate_to_view
|
|
|
|
|
+ {
|
|
|
|
|
+ nlohmann::json props;
|
|
|
|
|
+ props["view_id"] = {{"type", "string"}, {"description", "View ID to navigate to"}};
|
|
|
|
|
+ props["collection_name"] = {{"type", "string"}, {"description", "Alternative: Collection name to find the default view"}};
|
|
|
|
|
+ props["filter"] = {{"type", "object"}, {"description", "Optional filter to apply to the view"}};
|
|
|
|
|
+
|
|
|
|
|
+ RegisterTool({
|
|
|
|
|
+ .name = "navigate_to_view",
|
|
|
|
|
+ .description = "Navigate the user to a view to see collection data. Use this when the user wants to browse, search, or filter records.",
|
|
|
|
|
+ .input_schema = MakeObjectSchema(props),
|
|
|
|
|
+ .category = "navigation",
|
|
|
|
|
+ .permissions = {
|
|
|
|
|
+ .any_of = {std::string(permissions::kCollectionReadAll), std::string(permissions::kCollectionReadOwn)},
|
|
|
|
|
+ .all_of = {},
|
|
|
|
|
+ .requires_workspace_context = true
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // navigate_to_document
|
|
|
|
|
+ {
|
|
|
|
|
+ nlohmann::json props;
|
|
|
|
|
+ props["view_id"] = {{"type", "string"}, {"description", "View ID for the document"}};
|
|
|
|
|
+ props["document_id"] = {{"type", "string"}, {"description", "Document ID to edit (omit for create)"}};
|
|
|
|
|
+ props["action"] = {{"type", "string"}, {"enum", nlohmann::json::array({"create", "edit"})}, {"description", "Create new or edit existing document"}};
|
|
|
|
|
+
|
|
|
|
|
+ RegisterTool({
|
|
|
|
|
+ .name = "navigate_to_document",
|
|
|
|
|
+ .description = "Navigate to create a new document or edit an existing one. Use this when the user wants to add or modify a record.",
|
|
|
|
|
+ .input_schema = MakeObjectSchema(props, {"view_id", "action"}),
|
|
|
|
|
+ .category = "navigation",
|
|
|
|
|
+ .permissions = {
|
|
|
|
|
+ .any_of = {
|
|
|
|
|
+ std::string(permissions::kCollectionCreate),
|
|
|
|
|
+ std::string(permissions::kCollectionWriteAll),
|
|
|
|
|
+ std::string(permissions::kCollectionWriteOwn)
|
|
|
|
|
+ },
|
|
|
|
|
+ .all_of = {},
|
|
|
|
|
+ .requires_workspace_context = true
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // navigate_to_dashboard
|
|
|
|
|
+ {
|
|
|
|
|
+ RegisterTool({
|
|
|
|
|
+ .name = "navigate_to_dashboard",
|
|
|
|
|
+ .description = "Navigate the user to the main dashboard. Use this when the user wants to go to the home page or main view.",
|
|
|
|
|
+ .input_schema = MakeObjectSchema(nlohmann::json::object()),
|
|
|
|
|
+ .category = "navigation",
|
|
|
|
|
+ .permissions = {
|
|
|
|
|
+ .any_of = {},
|
|
|
|
|
+ .all_of = {},
|
|
|
|
|
+ .requires_workspace_context = false
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // navigate_to_workspace_settings
|
|
|
|
|
+ {
|
|
|
|
|
+ RegisterTool({
|
|
|
|
|
+ .name = "navigate_to_workspace_settings",
|
|
|
|
|
+ .description = "Navigate to workspace settings. Use this when the user wants to configure workspace options.",
|
|
|
|
|
+ .input_schema = MakeObjectSchema(nlohmann::json::object()),
|
|
|
|
|
+ .category = "navigation",
|
|
|
|
|
+ .permissions = {
|
|
|
|
|
+ .any_of = {std::string(permissions::kWorkspaceAdmin)},
|
|
|
|
|
+ .all_of = {},
|
|
|
|
|
+ .requires_workspace_context = true
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // navigate_to_user_management
|
|
|
|
|
+ {
|
|
|
|
|
+ RegisterTool({
|
|
|
|
|
+ .name = "navigate_to_user_management",
|
|
|
|
|
+ .description = "Navigate to user management. Use this when the user wants to manage users and groups.",
|
|
|
|
|
+ .input_schema = MakeObjectSchema(nlohmann::json::object()),
|
|
|
|
|
+ .category = "navigation",
|
|
|
|
|
+ .permissions = {
|
|
|
|
|
+ .any_of = {std::string(permissions::kUsersRead)},
|
|
|
|
|
+ .all_of = {},
|
|
|
|
|
+ .requires_workspace_context = false
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ spdlog::debug("Registered {} navigation tools", 6);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// =============================================================================
|
|
|
|
|
+// Workspace Tools Registration
|
|
|
|
|
+// =============================================================================
|
|
|
|
|
+
|
|
|
|
|
+void ToolService::RegisterWorkspaceTools() {
|
|
|
|
|
+ // create_workspace
|
|
|
|
|
+ {
|
|
|
|
|
+ nlohmann::json props;
|
|
|
|
|
+ props["name"] = {{"type", "string"}, {"description", "Name for the new workspace"}};
|
|
|
|
|
+ props["settings"] = {{"type", "object"}, {"description", "Optional workspace settings"}};
|
|
|
|
|
+
|
|
|
|
|
+ RegisterTool(
|
|
|
|
|
+ {
|
|
|
|
|
+ .name = "create_workspace",
|
|
|
|
|
+ .description = "Create a new workspace. Use this when the user wants to set up a new project or organization space.",
|
|
|
|
|
+ .input_schema = MakeObjectSchema(props, {"name"}),
|
|
|
|
|
+ .category = "workspace",
|
|
|
|
|
+ .permissions = {
|
|
|
|
|
+ .any_of = {std::string(permissions::kWorkspacesCreate)},
|
|
|
|
|
+ .all_of = {},
|
|
|
|
|
+ .requires_workspace_context = false
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ [this](const nlohmann::json& args, const ToolExecutionContext& ctx) {
|
|
|
|
|
+ return HandleCreateWorkspace(args, ctx);
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // list_workspaces
|
|
|
|
|
+ {
|
|
|
|
|
+ RegisterTool(
|
|
|
|
|
+ {
|
|
|
|
|
+ .name = "list_workspaces",
|
|
|
|
|
+ .description = "List all workspaces the user has access to. Use this to help the user find or switch workspaces.",
|
|
|
|
|
+ .input_schema = MakeObjectSchema(nlohmann::json::object()),
|
|
|
|
|
+ .category = "workspace",
|
|
|
|
|
+ .permissions = {
|
|
|
|
|
+ .any_of = {std::string(permissions::kWorkspacesRead)},
|
|
|
|
|
+ .all_of = {},
|
|
|
|
|
+ .requires_workspace_context = false
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ [this](const nlohmann::json& args, const ToolExecutionContext& ctx) {
|
|
|
|
|
+ return HandleListWorkspaces(args, ctx);
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // get_workspace_settings
|
|
|
|
|
+ {
|
|
|
|
|
+ nlohmann::json props;
|
|
|
|
|
+ props["workspace_id"] = {{"type", "string"}, {"description", "Workspace ID to get settings from"}};
|
|
|
|
|
+
|
|
|
|
|
+ RegisterTool(
|
|
|
|
|
+ {
|
|
|
|
|
+ .name = "get_workspace_settings",
|
|
|
|
|
+ .description = "Get settings from a workspace. Use this to retrieve configuration that can be copied to a new workspace.",
|
|
|
|
|
+ .input_schema = MakeObjectSchema(props, {"workspace_id"}),
|
|
|
|
|
+ .category = "workspace",
|
|
|
|
|
+ .permissions = {
|
|
|
|
|
+ .any_of = {std::string(permissions::kWorkspacesRead)},
|
|
|
|
|
+ .all_of = {},
|
|
|
|
|
+ .requires_workspace_context = false
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ [this](const nlohmann::json& args, const ToolExecutionContext& ctx) {
|
|
|
|
|
+ return HandleGetWorkspaceSettings(args, ctx);
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ spdlog::debug("Registered {} workspace tools", 3);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// =============================================================================
|
|
|
|
|
+// View Tools Registration
|
|
|
|
|
+// =============================================================================
|
|
|
|
|
+
|
|
|
|
|
+void ToolService::RegisterViewTools() {
|
|
|
|
|
+ // create_view
|
|
|
|
|
+ {
|
|
|
|
|
+ nlohmann::json props;
|
|
|
|
|
+ props["name"] = {{"type", "string"}, {"description", "Name for the view"}};
|
|
|
|
|
+ props["collection_name"] = {{"type", "string"}, {"description", "Collection name this view displays"}};
|
|
|
|
|
+
|
|
|
|
|
+ nlohmann::json field_props;
|
|
|
|
|
+ field_props["name"] = {{"type", "string"}};
|
|
|
|
|
+ field_props["type"] = {{"type", "string"}, {"enum", nlohmann::json::array({"text", "number", "date", "datetime", "boolean", "select", "reference", "email", "url", "color"})}};
|
|
|
|
|
+ field_props["label"] = {{"type", "string"}};
|
|
|
|
|
+ field_props["required"] = {{"type", "boolean"}};
|
|
|
|
|
+ field_props["options"] = {{"type", "array"}};
|
|
|
|
|
+
|
|
|
|
|
+ nlohmann::json schema_props;
|
|
|
|
|
+ schema_props["title"] = {{"type", "string"}};
|
|
|
|
|
+ schema_props["description"] = {{"type", "string"}};
|
|
|
|
|
+ schema_props["fields"] = {{"type", "array"}, {"items", {{"type", "object"}, {"properties", field_props}}}};
|
|
|
|
|
+
|
|
|
|
|
+ props["schema"] = {{"type", "object"}, {"description", "View schema with fields, title, description"}, {"properties", schema_props}};
|
|
|
|
|
+ props["settings"] = {{"type", "object"}, {"description", "View settings (is_default, show_in_sidebar, icon, etc.)"}};
|
|
|
|
|
+
|
|
|
|
|
+ RegisterTool(
|
|
|
|
|
+ {
|
|
|
|
|
+ .name = "create_view",
|
|
|
|
|
+ .description = "Create a new view with a schema for a collection. Use this to define how data should be displayed and edited.",
|
|
|
|
|
+ .input_schema = MakeObjectSchema(props, {"name", "collection_name"}),
|
|
|
|
|
+ .category = "view",
|
|
|
|
|
+ .permissions = {
|
|
|
|
|
+ .any_of = {std::string(permissions::kWorkspaceManageViews)},
|
|
|
|
|
+ .all_of = {},
|
|
|
|
|
+ .requires_workspace_context = true
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ [this](const nlohmann::json& args, const ToolExecutionContext& ctx) {
|
|
|
|
|
+ return HandleCreateView(args, ctx);
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // update_view
|
|
|
|
|
+ {
|
|
|
|
|
+ nlohmann::json props;
|
|
|
|
|
+ props["view_id"] = {{"type", "string"}, {"description", "View ID to update"}};
|
|
|
|
|
+ props["name"] = {{"type", "string"}, {"description", "New name for the view"}};
|
|
|
|
|
+ props["schema"] = {{"type", "object"}, {"description", "Updated view schema"}};
|
|
|
|
|
+ props["settings"] = {{"type", "object"}, {"description", "Updated view settings"}};
|
|
|
|
|
+
|
|
|
|
|
+ RegisterTool(
|
|
|
|
|
+ {
|
|
|
|
|
+ .name = "update_view",
|
|
|
|
|
+ .description = "Update an existing view's schema or settings.",
|
|
|
|
|
+ .input_schema = MakeObjectSchema(props, {"view_id"}),
|
|
|
|
|
+ .category = "view",
|
|
|
|
|
+ .permissions = {
|
|
|
|
|
+ .any_of = {std::string(permissions::kWorkspaceManageViews)},
|
|
|
|
|
+ .all_of = {},
|
|
|
|
|
+ .requires_workspace_context = true
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ [this](const nlohmann::json& args, const ToolExecutionContext& ctx) {
|
|
|
|
|
+ return HandleUpdateView(args, ctx);
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // list_views
|
|
|
|
|
+ {
|
|
|
|
|
+ nlohmann::json props;
|
|
|
|
|
+ props["collection_name"] = {{"type", "string"}, {"description", "Optional: filter to views for a specific collection"}};
|
|
|
|
|
+
|
|
|
|
|
+ RegisterTool(
|
|
|
|
|
+ {
|
|
|
|
|
+ .name = "list_views",
|
|
|
|
|
+ .description = "List all views in the current workspace. Use this to see available data views.",
|
|
|
|
|
+ .input_schema = MakeObjectSchema(props),
|
|
|
|
|
+ .category = "view",
|
|
|
|
|
+ .permissions = {
|
|
|
|
|
+ .any_of = {std::string(permissions::kViewsRead)},
|
|
|
|
|
+ .all_of = {},
|
|
|
|
|
+ .requires_workspace_context = true
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ [this](const nlohmann::json& args, const ToolExecutionContext& ctx) {
|
|
|
|
|
+ return HandleListViews(args, ctx);
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // get_view
|
|
|
|
|
+ {
|
|
|
|
|
+ nlohmann::json props;
|
|
|
|
|
+ props["view_id"] = {{"type", "string"}, {"description", "View ID to retrieve"}};
|
|
|
|
|
+
|
|
|
|
|
+ RegisterTool(
|
|
|
|
|
+ {
|
|
|
|
|
+ .name = "get_view",
|
|
|
|
|
+ .description = "Get details of a specific view including its schema.",
|
|
|
|
|
+ .input_schema = MakeObjectSchema(props, {"view_id"}),
|
|
|
|
|
+ .category = "view",
|
|
|
|
|
+ .permissions = {
|
|
|
|
|
+ .any_of = {std::string(permissions::kViewsRead)},
|
|
|
|
|
+ .all_of = {},
|
|
|
|
|
+ .requires_workspace_context = true
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ [this](const nlohmann::json& args, const ToolExecutionContext& ctx) {
|
|
|
|
|
+ return HandleGetView(args, ctx);
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ spdlog::debug("Registered {} view tools", 4);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// =============================================================================
|
|
|
|
|
+// Page Tools Registration
|
|
|
|
|
+// =============================================================================
|
|
|
|
|
+
|
|
|
|
|
+void ToolService::RegisterPageTools() {
|
|
|
|
|
+ // create_page
|
|
|
|
|
+ {
|
|
|
|
|
+ nlohmann::json props;
|
|
|
|
|
+ props["name"] = {{"type", "string"}, {"description", "Name for the page"}};
|
|
|
|
|
+ props["slug"] = {{"type", "string"}, {"description", "URL-friendly slug (auto-generated if not provided)"}};
|
|
|
|
|
+ props["layout"] = {{"type", "object"}, {"description", "Page layout with components"}, {"properties", {{"components", {{"type", "array"}, {"description", "Layout components"}}}}}};
|
|
|
|
|
+ props["settings"] = {{"type", "object"}, {"description", "Page settings (show_in_sidebar, icon, menu_order)"}};
|
|
|
|
|
+
|
|
|
|
|
+ RegisterTool(
|
|
|
|
|
+ {
|
|
|
|
|
+ .name = "create_page",
|
|
|
|
|
+ .description = "Create a new dashboard page with a layout. Use this to build custom dashboards.",
|
|
|
|
|
+ .input_schema = MakeObjectSchema(props, {"name"}),
|
|
|
|
|
+ .category = "page",
|
|
|
|
|
+ .permissions = {
|
|
|
|
|
+ .any_of = {std::string(permissions::kPageCreate)},
|
|
|
|
|
+ .all_of = {},
|
|
|
|
|
+ .requires_workspace_context = true
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ [this](const nlohmann::json& args, const ToolExecutionContext& ctx) {
|
|
|
|
|
+ return HandleCreatePage(args, ctx);
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // update_page
|
|
|
|
|
+ {
|
|
|
|
|
+ nlohmann::json props;
|
|
|
|
|
+ props["page_id"] = {{"type", "string"}, {"description", "Page ID to update"}};
|
|
|
|
|
+ props["name"] = {{"type", "string"}, {"description", "New name for the page"}};
|
|
|
|
|
+ props["layout"] = {{"type", "object"}, {"description", "Updated page layout"}};
|
|
|
|
|
+ props["settings"] = {{"type", "object"}, {"description", "Updated page settings"}};
|
|
|
|
|
+
|
|
|
|
|
+ RegisterTool(
|
|
|
|
|
+ {
|
|
|
|
|
+ .name = "update_page",
|
|
|
|
|
+ .description = "Update an existing page's layout or settings.",
|
|
|
|
|
+ .input_schema = MakeObjectSchema(props, {"page_id"}),
|
|
|
|
|
+ .category = "page",
|
|
|
|
|
+ .permissions = {
|
|
|
|
|
+ .any_of = {std::string(permissions::kPageWriteAll), std::string(permissions::kPageWriteOwn)},
|
|
|
|
|
+ .all_of = {},
|
|
|
|
|
+ .requires_workspace_context = true
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ [this](const nlohmann::json& args, const ToolExecutionContext& ctx) {
|
|
|
|
|
+ return HandleUpdatePage(args, ctx);
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // list_pages
|
|
|
|
|
+ {
|
|
|
|
|
+ RegisterTool(
|
|
|
|
|
+ {
|
|
|
|
|
+ .name = "list_pages",
|
|
|
|
|
+ .description = "List all pages in the current workspace.",
|
|
|
|
|
+ .input_schema = MakeObjectSchema(nlohmann::json::object()),
|
|
|
|
|
+ .category = "page",
|
|
|
|
|
+ .permissions = {
|
|
|
|
|
+ .any_of = {std::string(permissions::kPageReadAll), std::string(permissions::kPageReadOwn)},
|
|
|
|
|
+ .all_of = {},
|
|
|
|
|
+ .requires_workspace_context = true
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ [this](const nlohmann::json& args, const ToolExecutionContext& ctx) {
|
|
|
|
|
+ return HandleListPages(args, ctx);
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ spdlog::debug("Registered {} page tools", 3);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// =============================================================================
|
|
|
|
|
+// Document Tools Registration
|
|
|
|
|
+// =============================================================================
|
|
|
|
|
+
|
|
|
|
|
+void ToolService::RegisterDocumentTools() {
|
|
|
|
|
+ // create_document
|
|
|
|
|
+ {
|
|
|
|
|
+ nlohmann::json props;
|
|
|
|
|
+ props["collection"] = {{"type", "string"}, {"description", "Collection name to create document in"}};
|
|
|
|
|
+ props["data"] = {{"type", "object"}, {"description", "Document data fields"}};
|
|
|
|
|
+
|
|
|
|
|
+ RegisterTool(
|
|
|
|
|
+ {
|
|
|
|
|
+ .name = "create_document",
|
|
|
|
|
+ .description = "Create a new document/record in a collection. Use this to add data to the system.",
|
|
|
|
|
+ .input_schema = MakeObjectSchema(props, {"collection", "data"}),
|
|
|
|
|
+ .category = "document",
|
|
|
|
|
+ .permissions = {
|
|
|
|
|
+ .any_of = {std::string(permissions::kCollectionCreate)},
|
|
|
|
|
+ .all_of = {},
|
|
|
|
|
+ .requires_workspace_context = true
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ [this](const nlohmann::json& args, const ToolExecutionContext& ctx) {
|
|
|
|
|
+ return HandleCreateDocument(args, ctx);
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // update_document
|
|
|
|
|
+ {
|
|
|
|
|
+ nlohmann::json props;
|
|
|
|
|
+ props["collection"] = {{"type", "string"}, {"description", "Collection name"}};
|
|
|
|
|
+ props["document_id"] = {{"type", "string"}, {"description", "Document ID to update"}};
|
|
|
|
|
+ props["data"] = {{"type", "object"}, {"description", "Updated data fields (merged with existing)"}};
|
|
|
|
|
+
|
|
|
|
|
+ RegisterTool(
|
|
|
|
|
+ {
|
|
|
|
|
+ .name = "update_document",
|
|
|
|
|
+ .description = "Update an existing document/record.",
|
|
|
|
|
+ .input_schema = MakeObjectSchema(props, {"collection", "document_id", "data"}),
|
|
|
|
|
+ .category = "document",
|
|
|
|
|
+ .permissions = {
|
|
|
|
|
+ .any_of = {std::string(permissions::kCollectionWriteAll), std::string(permissions::kCollectionWriteOwn)},
|
|
|
|
|
+ .all_of = {},
|
|
|
|
|
+ .requires_workspace_context = true
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ [this](const nlohmann::json& args, const ToolExecutionContext& ctx) {
|
|
|
|
|
+ return HandleUpdateDocument(args, ctx);
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // get_document
|
|
|
|
|
+ {
|
|
|
|
|
+ nlohmann::json props;
|
|
|
|
|
+ props["collection"] = {{"type", "string"}, {"description", "Collection name"}};
|
|
|
|
|
+ props["document_id"] = {{"type", "string"}, {"description", "Document ID to retrieve"}};
|
|
|
|
|
+
|
|
|
|
|
+ RegisterTool(
|
|
|
|
|
+ {
|
|
|
|
|
+ .name = "get_document",
|
|
|
|
|
+ .description = "Get a specific document by ID.",
|
|
|
|
|
+ .input_schema = MakeObjectSchema(props, {"collection", "document_id"}),
|
|
|
|
|
+ .category = "document",
|
|
|
|
|
+ .permissions = {
|
|
|
|
|
+ .any_of = {std::string(permissions::kCollectionReadAll), std::string(permissions::kCollectionReadOwn)},
|
|
|
|
|
+ .all_of = {},
|
|
|
|
|
+ .requires_workspace_context = true
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ [this](const nlohmann::json& args, const ToolExecutionContext& ctx) {
|
|
|
|
|
+ return HandleGetDocument(args, ctx);
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // list_documents
|
|
|
|
|
+ {
|
|
|
|
|
+ nlohmann::json props;
|
|
|
|
|
+ props["collection"] = {{"type", "string"}, {"description", "Collection name to query"}};
|
|
|
|
|
+ props["filter"] = {{"type", "object"}, {"description", "Filter conditions (field: value pairs)"}};
|
|
|
|
|
+ props["sort_field"] = {{"type", "string"}, {"description", "Field to sort by"}};
|
|
|
|
|
+ props["sort_ascending"] = {{"type", "boolean"}, {"description", "Sort ascending (default: true)"}};
|
|
|
|
|
+ props["limit"] = {{"type", "integer"}, {"description", "Maximum documents to return (default: 20)"}};
|
|
|
|
|
+
|
|
|
|
|
+ RegisterTool(
|
|
|
|
|
+ {
|
|
|
|
|
+ .name = "list_documents",
|
|
|
|
|
+ .description = "Query and list documents in a collection with optional filtering.",
|
|
|
|
|
+ .input_schema = MakeObjectSchema(props, {"collection"}),
|
|
|
|
|
+ .category = "document",
|
|
|
|
|
+ .permissions = {
|
|
|
|
|
+ .any_of = {std::string(permissions::kCollectionReadAll), std::string(permissions::kCollectionReadOwn)},
|
|
|
|
|
+ .all_of = {},
|
|
|
|
|
+ .requires_workspace_context = true
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ [this](const nlohmann::json& args, const ToolExecutionContext& ctx) {
|
|
|
|
|
+ return HandleListDocuments(args, ctx);
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // delete_document
|
|
|
|
|
+ {
|
|
|
|
|
+ nlohmann::json props;
|
|
|
|
|
+ props["collection"] = {{"type", "string"}, {"description", "Collection name"}};
|
|
|
|
|
+ props["document_id"] = {{"type", "string"}, {"description", "Document ID to delete"}};
|
|
|
|
|
+
|
|
|
|
|
+ RegisterTool(
|
|
|
|
|
+ {
|
|
|
|
|
+ .name = "delete_document",
|
|
|
|
|
+ .description = "Delete a document from a collection.",
|
|
|
|
|
+ .input_schema = MakeObjectSchema(props, {"collection", "document_id"}),
|
|
|
|
|
+ .category = "document",
|
|
|
|
|
+ .permissions = {
|
|
|
|
|
+ .any_of = {std::string(permissions::kCollectionDeleteAll), std::string(permissions::kCollectionDeleteOwn)},
|
|
|
|
|
+ .all_of = {},
|
|
|
|
|
+ .requires_workspace_context = true
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ [this](const nlohmann::json& args, const ToolExecutionContext& ctx) {
|
|
|
|
|
+ return HandleDeleteDocument(args, ctx);
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ spdlog::debug("Registered {} document tools", 5);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// =============================================================================
|
|
|
|
|
+// Navigation Tool Handlers
|
|
|
|
|
+// =============================================================================
|
|
|
|
|
+
|
|
|
|
|
+auto ToolService::HandleNavigateToPage(const nlohmann::json& args,
|
|
|
|
|
+ const ToolExecutionContext& ctx) -> ToolExecutionResult {
|
|
|
|
|
+ std::string slug;
|
|
|
|
|
+ std::string page_id;
|
|
|
|
|
+
|
|
|
|
|
+ if (args.contains("page_slug") && args["page_slug"].is_string()) {
|
|
|
|
|
+ slug = args["page_slug"].get<std::string>();
|
|
|
|
|
+ }
|
|
|
|
|
+ if (args.contains("page_id") && args["page_id"].is_string()) {
|
|
|
|
|
+ page_id = args["page_id"].get<std::string>();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (slug.empty() && page_id.empty()) {
|
|
|
|
|
+ return {false, "Either page_slug or page_id is required", true, std::nullopt};
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Verify page exists and user can access it
|
|
|
|
|
+ PageResult page_result;
|
|
|
|
|
+ if (!slug.empty()) {
|
|
|
|
|
+ page_result = page_service_.GetPageBySlug(ctx.workspace_id, slug);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ page_result = page_service_.GetPage(ctx.workspace_id, page_id);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!page_result.success || !page_result.page) {
|
|
|
|
|
+ return {false, "Page not found", true, std::nullopt};
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Check permission
|
|
|
|
|
+ if (!authorization_service_.CanViewPage(ctx.user, ctx.workspace_id,
|
|
|
|
|
+ page_result.page->created_by,
|
|
|
|
|
+ page_result.page->shared_with_groups)) {
|
|
|
|
|
+ return {false, "Permission denied to view this page", true, std::nullopt};
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ ToolExecutionResult result;
|
|
|
|
|
+ result.success = true;
|
|
|
|
|
+ result.content = "Navigating to page: " + page_result.page->name;
|
|
|
|
|
+ result.navigation = ToolExecutionResult::NavigationData{
|
|
|
|
|
+ .type = "page",
|
|
|
|
|
+ .slug = page_result.page->slug,
|
|
|
|
|
+ .view_id = "",
|
|
|
|
|
+ .document_id = "",
|
|
|
|
|
+ .filter = {}
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ return result;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+auto ToolService::HandleNavigateToView(const nlohmann::json& args,
|
|
|
|
|
+ const ToolExecutionContext& ctx) -> ToolExecutionResult {
|
|
|
|
|
+ std::string view_id;
|
|
|
|
|
+ std::string collection_name;
|
|
|
|
|
+ nlohmann::json filter;
|
|
|
|
|
+
|
|
|
|
|
+ if (args.contains("view_id") && args["view_id"].is_string()) {
|
|
|
|
|
+ view_id = args["view_id"].get<std::string>();
|
|
|
|
|
+ }
|
|
|
|
|
+ if (args.contains("collection_name") && args["collection_name"].is_string()) {
|
|
|
|
|
+ collection_name = args["collection_name"].get<std::string>();
|
|
|
|
|
+ }
|
|
|
|
|
+ if (args.contains("filter") && args["filter"].is_object()) {
|
|
|
|
|
+ filter = args["filter"];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Get view by ID or find by collection
|
|
|
|
|
+ ViewResult view_result;
|
|
|
|
|
+ if (!view_id.empty()) {
|
|
|
|
|
+ view_result = view_service_.GetView(ctx.workspace_id, view_id);
|
|
|
|
|
+ } else if (!collection_name.empty()) {
|
|
|
|
|
+ // Find default view for collection
|
|
|
|
|
+ auto views = view_service_.ListViewsForCollection(ctx.workspace_id, collection_name);
|
|
|
|
|
+ if (views.success && !views.views.empty()) {
|
|
|
|
|
+ // Find default or use first
|
|
|
|
|
+ for (const auto& v : views.views) {
|
|
|
|
|
+ if (v.settings.is_default) {
|
|
|
|
|
+ view_result.success = true;
|
|
|
|
|
+ view_result.view = v;
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!view_result.view) {
|
|
|
|
|
+ view_result.success = true;
|
|
|
|
|
+ view_result.view = views.views[0];
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!view_result.success || !view_result.view) {
|
|
|
|
|
+ return {false, "View not found", true, std::nullopt};
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Check permission for collection
|
|
|
|
|
+ if (!authorization_service_.CanReadCollection(ctx.user, ctx.workspace_id,
|
|
|
|
|
+ view_result.view->collection_name)) {
|
|
|
|
|
+ return {false, "Permission denied to view this collection", true, std::nullopt};
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ ToolExecutionResult result;
|
|
|
|
|
+ result.success = true;
|
|
|
|
|
+ result.content = "Navigating to view: " + view_result.view->name;
|
|
|
|
|
+ result.navigation = ToolExecutionResult::NavigationData{
|
|
|
|
|
+ .type = "view",
|
|
|
|
|
+ .slug = "",
|
|
|
|
|
+ .view_id = view_result.view->id,
|
|
|
|
|
+ .document_id = "",
|
|
|
|
|
+ .filter = filter
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ return result;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+auto ToolService::HandleNavigateToDocument(const nlohmann::json& args,
|
|
|
|
|
+ const ToolExecutionContext& ctx) -> ToolExecutionResult {
|
|
|
|
|
+ if (!args.contains("view_id") || !args["view_id"].is_string()) {
|
|
|
|
|
+ return {false, "view_id is required", true, std::nullopt};
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!args.contains("action") || !args["action"].is_string()) {
|
|
|
|
|
+ return {false, "action is required", true, std::nullopt};
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ std::string view_id = args["view_id"].get<std::string>();
|
|
|
|
|
+ std::string action = args["action"].get<std::string>();
|
|
|
|
|
+ std::string document_id;
|
|
|
|
|
+
|
|
|
|
|
+ if (args.contains("document_id") && args["document_id"].is_string()) {
|
|
|
|
|
+ document_id = args["document_id"].get<std::string>();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (action != "create" && action != "edit") {
|
|
|
|
|
+ return {false, "action must be 'create' or 'edit'", true, std::nullopt};
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (action == "edit" && document_id.empty()) {
|
|
|
|
|
+ return {false, "document_id is required for edit action", true, std::nullopt};
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Verify view exists
|
|
|
|
|
+ auto view_result = view_service_.GetView(ctx.workspace_id, view_id);
|
|
|
|
|
+ if (!view_result.success || !view_result.view) {
|
|
|
|
|
+ return {false, "View not found", true, std::nullopt};
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Check permission
|
|
|
|
|
+ if (action == "create") {
|
|
|
|
|
+ if (!authorization_service_.CanCreateDocument(ctx.user, ctx.workspace_id,
|
|
|
|
|
+ view_result.view->collection_name)) {
|
|
|
|
|
+ return {false, "Permission denied to create documents", true, std::nullopt};
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // For edit, we need to check the document owner
|
|
|
|
|
+ auto doc_result = document_service_.GetDocument(ctx.workspace_id,
|
|
|
|
|
+ view_result.view->collection_name,
|
|
|
|
|
+ document_id);
|
|
|
|
|
+ if (!doc_result.success || !doc_result.document) {
|
|
|
|
|
+ return {false, "Document not found", true, std::nullopt};
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ std::string owner;
|
|
|
|
|
+ if (doc_result.document->data.contains("_created_by")) {
|
|
|
|
|
+ owner = doc_result.document->data["_created_by"].get<std::string>();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!authorization_service_.CanWriteDocument(ctx.user, ctx.workspace_id,
|
|
|
|
|
+ view_result.view->collection_name, owner)) {
|
|
|
|
|
+ return {false, "Permission denied to edit this document", true, std::nullopt};
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ ToolExecutionResult result;
|
|
|
|
|
+ result.success = true;
|
|
|
|
|
+ result.content = action == "create"
|
|
|
|
|
+ ? "Navigating to create new document"
|
|
|
|
|
+ : "Navigating to edit document: " + document_id;
|
|
|
|
|
+ result.navigation = ToolExecutionResult::NavigationData{
|
|
|
|
|
+ .type = action == "create" ? "document_create" : "document_edit",
|
|
|
|
|
+ .slug = "",
|
|
|
|
|
+ .view_id = view_id,
|
|
|
|
|
+ .document_id = document_id,
|
|
|
|
|
+ .filter = {}
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ return result;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+auto ToolService::HandleNavigateToDashboard(const nlohmann::json& /*args*/,
|
|
|
|
|
+ const ToolExecutionContext& /*ctx*/) -> ToolExecutionResult {
|
|
|
|
|
+ ToolExecutionResult result;
|
|
|
|
|
+ result.success = true;
|
|
|
|
|
+ result.content = "Navigating to dashboard";
|
|
|
|
|
+ result.navigation = ToolExecutionResult::NavigationData{
|
|
|
|
|
+ .type = "dashboard",
|
|
|
|
|
+ .slug = "",
|
|
|
|
|
+ .view_id = "",
|
|
|
|
|
+ .document_id = "",
|
|
|
|
|
+ .filter = {}
|
|
|
|
|
+ };
|
|
|
|
|
+ return result;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+auto ToolService::HandleNavigateToWorkspaceSettings(const nlohmann::json& /*args*/,
|
|
|
|
|
+ const ToolExecutionContext& ctx) -> ToolExecutionResult {
|
|
|
|
|
+ if (!authorization_service_.IsWorkspaceAdmin(ctx.user, ctx.workspace_id)) {
|
|
|
|
|
+ return {false, "Permission denied - workspace admin required", true, std::nullopt};
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ ToolExecutionResult result;
|
|
|
|
|
+ result.success = true;
|
|
|
|
|
+ result.content = "Navigating to workspace settings";
|
|
|
|
|
+ result.navigation = ToolExecutionResult::NavigationData{
|
|
|
|
|
+ .type = "settings",
|
|
|
|
|
+ .slug = "workspace",
|
|
|
|
|
+ .view_id = "",
|
|
|
|
|
+ .document_id = "",
|
|
|
|
|
+ .filter = {}
|
|
|
|
|
+ };
|
|
|
|
|
+ return result;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+auto ToolService::HandleNavigateToUserManagement(const nlohmann::json& /*args*/,
|
|
|
|
|
+ const ToolExecutionContext& /*ctx*/) -> ToolExecutionResult {
|
|
|
|
|
+ ToolExecutionResult result;
|
|
|
|
|
+ result.success = true;
|
|
|
|
|
+ result.content = "Navigating to user management";
|
|
|
|
|
+ result.navigation = ToolExecutionResult::NavigationData{
|
|
|
|
|
+ .type = "settings",
|
|
|
|
|
+ .slug = "users",
|
|
|
|
|
+ .view_id = "",
|
|
|
|
|
+ .document_id = "",
|
|
|
|
|
+ .filter = {}
|
|
|
|
|
+ };
|
|
|
|
|
+ return result;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// =============================================================================
|
|
|
|
|
+// Workspace Tool Handlers
|
|
|
|
|
+// =============================================================================
|
|
|
|
|
+
|
|
|
|
|
+auto ToolService::HandleCreateWorkspace(const nlohmann::json& args,
|
|
|
|
|
+ const ToolExecutionContext& ctx) -> ToolExecutionResult {
|
|
|
|
|
+ if (!args.contains("name") || !args["name"].is_string()) {
|
|
|
|
|
+ return {false, "name is required", true, std::nullopt};
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ CreateWorkspaceRequest request;
|
|
|
|
|
+ request.name = args["name"].get<std::string>();
|
|
|
|
|
+ request.actor_id = ctx.user_id;
|
|
|
|
|
+
|
|
|
|
|
+ if (args.contains("settings") && args["settings"].is_object()) {
|
|
|
|
|
+ request.settings = args["settings"];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ auto result = workspace_service_.CreateWorkspace(request);
|
|
|
|
|
+ if (!result.success) {
|
|
|
|
|
+ return {false, result.error, true, std::nullopt};
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ nlohmann::json response;
|
|
|
|
|
+ response["id"] = result.workspace->id;
|
|
|
|
|
+ response["name"] = result.workspace->name;
|
|
|
|
|
+ response["message"] = "Workspace created successfully";
|
|
|
|
|
+
|
|
|
|
|
+ return {true, response.dump(), false, std::nullopt};
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+auto ToolService::HandleListWorkspaces(const nlohmann::json& /*args*/,
|
|
|
|
|
+ const ToolExecutionContext& ctx) -> ToolExecutionResult {
|
|
|
|
|
+ // List workspaces the user has access to
|
|
|
|
|
+ auto result = workspace_service_.ListWorkspacesByIds(ctx.user.workspace_ids);
|
|
|
|
|
+ if (!result.success) {
|
|
|
|
|
+ return {false, result.error, true, std::nullopt};
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ nlohmann::json response = nlohmann::json::array();
|
|
|
|
|
+ for (const auto& ws : result.workspaces) {
|
|
|
|
|
+ response.push_back({
|
|
|
|
|
+ {"id", ws.id},
|
|
|
|
|
+ {"name", ws.name}
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return {true, response.dump(), false, std::nullopt};
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+auto ToolService::HandleGetWorkspaceSettings(const nlohmann::json& args,
|
|
|
|
|
+ const ToolExecutionContext& /*ctx*/) -> ToolExecutionResult {
|
|
|
|
|
+ if (!args.contains("workspace_id") || !args["workspace_id"].is_string()) {
|
|
|
|
|
+ return {false, "workspace_id is required", true, std::nullopt};
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ std::string workspace_id = args["workspace_id"].get<std::string>();
|
|
|
|
|
+ auto result = workspace_service_.GetWorkspace(workspace_id);
|
|
|
|
|
+
|
|
|
|
|
+ if (!result.success || !result.workspace) {
|
|
|
|
|
+ return {false, result.error.empty() ? "Workspace not found" : result.error, true, std::nullopt};
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ nlohmann::json response;
|
|
|
|
|
+ response["id"] = result.workspace->id;
|
|
|
|
|
+ response["name"] = result.workspace->name;
|
|
|
|
|
+ response["settings"] = result.workspace->settings;
|
|
|
|
|
+
|
|
|
|
|
+ return {true, response.dump(), false, std::nullopt};
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// =============================================================================
|
|
|
|
|
+// View Tool Handlers
|
|
|
|
|
+// =============================================================================
|
|
|
|
|
+
|
|
|
|
|
+auto ToolService::HandleCreateView(const nlohmann::json& args,
|
|
|
|
|
+ const ToolExecutionContext& ctx) -> ToolExecutionResult {
|
|
|
|
|
+ if (!args.contains("name") || !args["name"].is_string()) {
|
|
|
|
|
+ return {false, "name is required", true, std::nullopt};
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!args.contains("collection_name") || !args["collection_name"].is_string()) {
|
|
|
|
|
+ return {false, "collection_name is required", true, std::nullopt};
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ CreateViewRequest request;
|
|
|
|
|
+ request.workspace_id = ctx.workspace_id;
|
|
|
|
|
+ request.name = args["name"].get<std::string>();
|
|
|
|
|
+ request.collection_name = args["collection_name"].get<std::string>();
|
|
|
|
|
+
|
|
|
|
|
+ // Parse schema if provided
|
|
|
|
|
+ if (args.contains("schema") && args["schema"].is_object()) {
|
|
|
|
|
+ const auto& schema = args["schema"];
|
|
|
|
|
+ if (schema.contains("title")) {
|
|
|
|
|
+ request.schema.title = schema["title"].get<std::string>();
|
|
|
|
|
+ }
|
|
|
|
|
+ if (schema.contains("description")) {
|
|
|
|
|
+ request.schema.description = schema["description"].get<std::string>();
|
|
|
|
|
+ }
|
|
|
|
|
+ if (schema.contains("fields") && schema["fields"].is_array()) {
|
|
|
|
|
+ for (const auto& field : schema["fields"]) {
|
|
|
|
|
+ SchemaField sf;
|
|
|
|
|
+ if (field.contains("name")) sf.name = field["name"].get<std::string>();
|
|
|
|
|
+ if (field.contains("label")) sf.label = field["label"].get<std::string>();
|
|
|
|
|
+ if (field.contains("type")) {
|
|
|
|
|
+ sf.type = StringToFieldType(field["type"].get<std::string>());
|
|
|
|
|
+ }
|
|
|
|
|
+ if (field.contains("required")) sf.required = field["required"].get<bool>();
|
|
|
|
|
+ if (field.contains("options")) sf.options = field["options"];
|
|
|
|
|
+ request.schema.fields.push_back(sf);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Parse settings if provided
|
|
|
|
|
+ if (args.contains("settings") && args["settings"].is_object()) {
|
|
|
|
|
+ const auto& settings = args["settings"];
|
|
|
|
|
+ if (settings.contains("is_default")) {
|
|
|
|
|
+ request.settings.is_default = settings["is_default"].get<bool>();
|
|
|
|
|
+ }
|
|
|
|
|
+ if (settings.contains("show_in_sidebar")) {
|
|
|
|
|
+ request.settings.show_in_sidebar = settings["show_in_sidebar"].get<bool>();
|
|
|
|
|
+ }
|
|
|
|
|
+ if (settings.contains("icon")) {
|
|
|
|
|
+ request.settings.icon = settings["icon"].get<std::string>();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ auto result = view_service_.CreateView(request);
|
|
|
|
|
+ if (!result.success) {
|
|
|
|
|
+ return {false, result.error, true, std::nullopt};
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ nlohmann::json response;
|
|
|
|
|
+ response["id"] = result.view->id;
|
|
|
|
|
+ response["name"] = result.view->name;
|
|
|
|
|
+ response["collection_name"] = result.view->collection_name;
|
|
|
|
|
+ response["message"] = "View created successfully";
|
|
|
|
|
+
|
|
|
|
|
+ return {true, response.dump(), false, std::nullopt};
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+auto ToolService::HandleUpdateView(const nlohmann::json& args,
|
|
|
|
|
+ const ToolExecutionContext& ctx) -> ToolExecutionResult {
|
|
|
|
|
+ if (!args.contains("view_id") || !args["view_id"].is_string()) {
|
|
|
|
|
+ return {false, "view_id is required", true, std::nullopt};
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ UpdateViewRequest request;
|
|
|
|
|
+ request.workspace_id = ctx.workspace_id;
|
|
|
|
|
+ request.id = args["view_id"].get<std::string>();
|
|
|
|
|
+
|
|
|
|
|
+ if (args.contains("name") && args["name"].is_string()) {
|
|
|
|
|
+ request.name = args["name"].get<std::string>();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Parse schema if provided
|
|
|
|
|
+ if (args.contains("schema") && args["schema"].is_object()) {
|
|
|
|
|
+ ViewSchema schema;
|
|
|
|
|
+ const auto& s = args["schema"];
|
|
|
|
|
+ if (s.contains("title")) schema.title = s["title"].get<std::string>();
|
|
|
|
|
+ if (s.contains("description")) schema.description = s["description"].get<std::string>();
|
|
|
|
|
+ if (s.contains("fields") && s["fields"].is_array()) {
|
|
|
|
|
+ for (const auto& field : s["fields"]) {
|
|
|
|
|
+ SchemaField sf;
|
|
|
|
|
+ if (field.contains("name")) sf.name = field["name"].get<std::string>();
|
|
|
|
|
+ if (field.contains("label")) sf.label = field["label"].get<std::string>();
|
|
|
|
|
+ if (field.contains("type")) {
|
|
|
|
|
+ sf.type = StringToFieldType(field["type"].get<std::string>());
|
|
|
|
|
+ }
|
|
|
|
|
+ if (field.contains("required")) sf.required = field["required"].get<bool>();
|
|
|
|
|
+ if (field.contains("options")) sf.options = field["options"];
|
|
|
|
|
+ schema.fields.push_back(sf);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ request.schema = schema;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Parse settings if provided
|
|
|
|
|
+ if (args.contains("settings") && args["settings"].is_object()) {
|
|
|
|
|
+ ViewSettings settings;
|
|
|
|
|
+ const auto& s = args["settings"];
|
|
|
|
|
+ if (s.contains("is_default")) settings.is_default = s["is_default"].get<bool>();
|
|
|
|
|
+ if (s.contains("show_in_sidebar")) settings.show_in_sidebar = s["show_in_sidebar"].get<bool>();
|
|
|
|
|
+ if (s.contains("icon")) settings.icon = s["icon"].get<std::string>();
|
|
|
|
|
+ request.settings = settings;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ auto result = view_service_.UpdateView(request);
|
|
|
|
|
+ if (!result.success) {
|
|
|
|
|
+ return {false, result.error, true, std::nullopt};
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return {true, "View updated successfully", false, std::nullopt};
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+auto ToolService::HandleListViews(const nlohmann::json& args,
|
|
|
|
|
+ const ToolExecutionContext& ctx) -> ToolExecutionResult {
|
|
|
|
|
+ ViewListResult result;
|
|
|
|
|
+
|
|
|
|
|
+ if (args.contains("collection_name") && args["collection_name"].is_string()) {
|
|
|
|
|
+ result = view_service_.ListViewsForCollection(ctx.workspace_id,
|
|
|
|
|
+ args["collection_name"].get<std::string>());
|
|
|
|
|
+ } else {
|
|
|
|
|
+ result = view_service_.ListViews(ctx.workspace_id);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!result.success) {
|
|
|
|
|
+ return {false, result.error, true, std::nullopt};
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ nlohmann::json response = nlohmann::json::array();
|
|
|
|
|
+ for (const auto& view : result.views) {
|
|
|
|
|
+ response.push_back({
|
|
|
|
|
+ {"id", view.id},
|
|
|
|
|
+ {"name", view.name},
|
|
|
|
|
+ {"collection_name", view.collection_name},
|
|
|
|
|
+ {"is_default", view.settings.is_default}
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return {true, response.dump(), false, std::nullopt};
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+auto ToolService::HandleGetView(const nlohmann::json& args,
|
|
|
|
|
+ const ToolExecutionContext& ctx) -> ToolExecutionResult {
|
|
|
|
|
+ if (!args.contains("view_id") || !args["view_id"].is_string()) {
|
|
|
|
|
+ return {false, "view_id is required", true, std::nullopt};
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ auto result = view_service_.GetView(ctx.workspace_id, args["view_id"].get<std::string>());
|
|
|
|
|
+ if (!result.success || !result.view) {
|
|
|
|
|
+ return {false, result.error.empty() ? "View not found" : result.error, true, std::nullopt};
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ nlohmann::json response;
|
|
|
|
|
+ response["id"] = result.view->id;
|
|
|
|
|
+ response["name"] = result.view->name;
|
|
|
|
|
+ response["collection_name"] = result.view->collection_name;
|
|
|
|
|
+
|
|
|
|
|
+ // Include schema
|
|
|
|
|
+ nlohmann::json schema;
|
|
|
|
|
+ schema["title"] = result.view->schema.title;
|
|
|
|
|
+ schema["description"] = result.view->schema.description;
|
|
|
|
|
+
|
|
|
|
|
+ nlohmann::json fields = nlohmann::json::array();
|
|
|
|
|
+ for (const auto& field : result.view->schema.fields) {
|
|
|
|
|
+ fields.push_back({
|
|
|
|
|
+ {"name", field.name},
|
|
|
|
|
+ {"type", FieldTypeToString(field.type)},
|
|
|
|
|
+ {"label", field.label},
|
|
|
|
|
+ {"required", field.required}
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ schema["fields"] = fields;
|
|
|
|
|
+ response["schema"] = schema;
|
|
|
|
|
+
|
|
|
|
|
+ return {true, response.dump(), false, std::nullopt};
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// =============================================================================
|
|
|
|
|
+// Page Tool Handlers
|
|
|
|
|
+// =============================================================================
|
|
|
|
|
+
|
|
|
|
|
+auto ToolService::HandleCreatePage(const nlohmann::json& args,
|
|
|
|
|
+ const ToolExecutionContext& ctx) -> ToolExecutionResult {
|
|
|
|
|
+ if (!args.contains("name") || !args["name"].is_string()) {
|
|
|
|
|
+ return {false, "name is required", true, std::nullopt};
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ CreatePageRequest request;
|
|
|
|
|
+ request.workspace_id = ctx.workspace_id;
|
|
|
|
|
+ request.name = args["name"].get<std::string>();
|
|
|
|
|
+ request.created_by = ctx.user_id;
|
|
|
|
|
+
|
|
|
|
|
+ if (args.contains("slug") && args["slug"].is_string()) {
|
|
|
|
|
+ request.slug = args["slug"].get<std::string>();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (args.contains("layout") && args["layout"].is_object()) {
|
|
|
|
|
+ const auto& layout = args["layout"];
|
|
|
|
|
+ if (layout.contains("components") && layout["components"].is_array()) {
|
|
|
|
|
+ for (const auto& comp : layout["components"]) {
|
|
|
|
|
+ LayoutComponent lc;
|
|
|
|
|
+ if (comp.contains("id")) lc.id = comp["id"].get<std::string>();
|
|
|
|
|
+ if (comp.contains("type")) lc.type = comp["type"].get<std::string>();
|
|
|
|
|
+ if (comp.contains("config")) lc.config = comp["config"];
|
|
|
|
|
+ if (comp.contains("position") && comp["position"].is_object()) {
|
|
|
|
|
+ const auto& pos = comp["position"];
|
|
|
|
|
+ if (pos.contains("x")) lc.position.x = pos["x"].get<int>();
|
|
|
|
|
+ if (pos.contains("y")) lc.position.y = pos["y"].get<int>();
|
|
|
|
|
+ if (pos.contains("width")) lc.position.width = pos["width"].get<int>();
|
|
|
|
|
+ if (pos.contains("height")) lc.position.height = pos["height"].get<int>();
|
|
|
|
|
+ }
|
|
|
|
|
+ request.layout.components.push_back(lc);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (args.contains("settings") && args["settings"].is_object()) {
|
|
|
|
|
+ const auto& settings = args["settings"];
|
|
|
|
|
+ if (settings.contains("show_in_sidebar")) {
|
|
|
|
|
+ request.settings.show_in_sidebar = settings["show_in_sidebar"].get<bool>();
|
|
|
|
|
+ }
|
|
|
|
|
+ if (settings.contains("icon")) {
|
|
|
|
|
+ request.settings.icon = settings["icon"].get<std::string>();
|
|
|
|
|
+ }
|
|
|
|
|
+ if (settings.contains("menu_order")) {
|
|
|
|
|
+ request.settings.menu_order = settings["menu_order"].get<int>();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ auto result = page_service_.CreatePage(request);
|
|
|
|
|
+ if (!result.success) {
|
|
|
|
|
+ return {false, result.error, true, std::nullopt};
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ nlohmann::json response;
|
|
|
|
|
+ response["id"] = result.page->id;
|
|
|
|
|
+ response["name"] = result.page->name;
|
|
|
|
|
+ response["slug"] = result.page->slug;
|
|
|
|
|
+ response["message"] = "Page created successfully";
|
|
|
|
|
+
|
|
|
|
|
+ return {true, response.dump(), false, std::nullopt};
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+auto ToolService::HandleUpdatePage(const nlohmann::json& args,
|
|
|
|
|
+ const ToolExecutionContext& ctx) -> ToolExecutionResult {
|
|
|
|
|
+ if (!args.contains("page_id") || !args["page_id"].is_string()) {
|
|
|
|
|
+ return {false, "page_id is required", true, std::nullopt};
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ std::string page_id = args["page_id"].get<std::string>();
|
|
|
|
|
+
|
|
|
|
|
+ // Check ownership for write_own permission
|
|
|
|
|
+ auto existing = page_service_.GetPage(ctx.workspace_id, page_id);
|
|
|
|
|
+ if (!existing.success || !existing.page) {
|
|
|
|
|
+ return {false, "Page not found", true, std::nullopt};
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!authorization_service_.CanEditPage(ctx.user, ctx.workspace_id, existing.page->created_by)) {
|
|
|
|
|
+ return {false, "Permission denied to edit this page", true, std::nullopt};
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ UpdatePageRequest request;
|
|
|
|
|
+ request.workspace_id = ctx.workspace_id;
|
|
|
|
|
+ request.id = page_id;
|
|
|
|
|
+
|
|
|
|
|
+ if (args.contains("name") && args["name"].is_string()) {
|
|
|
|
|
+ request.name = args["name"].get<std::string>();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (args.contains("layout") && args["layout"].is_object()) {
|
|
|
|
|
+ PageLayout layout;
|
|
|
|
|
+ const auto& l = args["layout"];
|
|
|
|
|
+ if (l.contains("components") && l["components"].is_array()) {
|
|
|
|
|
+ for (const auto& comp : l["components"]) {
|
|
|
|
|
+ LayoutComponent lc;
|
|
|
|
|
+ if (comp.contains("id")) lc.id = comp["id"].get<std::string>();
|
|
|
|
|
+ if (comp.contains("type")) lc.type = comp["type"].get<std::string>();
|
|
|
|
|
+ if (comp.contains("config")) lc.config = comp["config"];
|
|
|
|
|
+ if (comp.contains("position") && comp["position"].is_object()) {
|
|
|
|
|
+ const auto& pos = comp["position"];
|
|
|
|
|
+ if (pos.contains("x")) lc.position.x = pos["x"].get<int>();
|
|
|
|
|
+ if (pos.contains("y")) lc.position.y = pos["y"].get<int>();
|
|
|
|
|
+ if (pos.contains("width")) lc.position.width = pos["width"].get<int>();
|
|
|
|
|
+ if (pos.contains("height")) lc.position.height = pos["height"].get<int>();
|
|
|
|
|
+ }
|
|
|
|
|
+ layout.components.push_back(lc);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ request.layout = layout;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (args.contains("settings") && args["settings"].is_object()) {
|
|
|
|
|
+ PageSettings settings;
|
|
|
|
|
+ const auto& s = args["settings"];
|
|
|
|
|
+ if (s.contains("show_in_sidebar")) settings.show_in_sidebar = s["show_in_sidebar"].get<bool>();
|
|
|
|
|
+ if (s.contains("icon")) settings.icon = s["icon"].get<std::string>();
|
|
|
|
|
+ if (s.contains("menu_order")) settings.menu_order = s["menu_order"].get<int>();
|
|
|
|
|
+ request.settings = settings;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ auto result = page_service_.UpdatePage(request);
|
|
|
|
|
+ if (!result.success) {
|
|
|
|
|
+ return {false, result.error, true, std::nullopt};
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return {true, "Page updated successfully", false, std::nullopt};
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+auto ToolService::HandleListPages(const nlohmann::json& /*args*/,
|
|
|
|
|
+ const ToolExecutionContext& ctx) -> ToolExecutionResult {
|
|
|
|
|
+ auto result = page_service_.ListPages(ctx.workspace_id);
|
|
|
|
|
+ if (!result.success) {
|
|
|
|
|
+ return {false, result.error, true, std::nullopt};
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ nlohmann::json response = nlohmann::json::array();
|
|
|
|
|
+ for (const auto& page : result.pages) {
|
|
|
|
|
+ // Filter by permission
|
|
|
|
|
+ if (authorization_service_.CanViewPage(ctx.user, ctx.workspace_id,
|
|
|
|
|
+ page.created_by, page.shared_with_groups)) {
|
|
|
|
|
+ response.push_back({
|
|
|
|
|
+ {"id", page.id},
|
|
|
|
|
+ {"name", page.name},
|
|
|
|
|
+ {"slug", page.slug},
|
|
|
|
|
+ {"show_in_sidebar", page.settings.show_in_sidebar}
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return {true, response.dump(), false, std::nullopt};
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// =============================================================================
|
|
|
|
|
+// Document Tool Handlers
|
|
|
|
|
+// =============================================================================
|
|
|
|
|
+
|
|
|
|
|
+auto ToolService::HandleCreateDocument(const nlohmann::json& args,
|
|
|
|
|
+ const ToolExecutionContext& ctx) -> ToolExecutionResult {
|
|
|
|
|
+ if (!args.contains("collection") || !args["collection"].is_string()) {
|
|
|
|
|
+ return {false, "collection is required", true, std::nullopt};
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!args.contains("data") || !args["data"].is_object()) {
|
|
|
|
|
+ return {false, "data is required", true, std::nullopt};
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ std::string collection = args["collection"].get<std::string>();
|
|
|
|
|
+
|
|
|
|
|
+ // Re-check permission for this specific collection
|
|
|
|
|
+ if (!authorization_service_.CanCreateDocument(ctx.user, ctx.workspace_id, collection)) {
|
|
|
|
|
+ return {false, "Permission denied to create documents in collection: " + collection, true, std::nullopt};
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ CreateDocumentRequest request;
|
|
|
|
|
+ request.workspace_id = ctx.workspace_id;
|
|
|
|
|
+ request.collection = collection;
|
|
|
|
|
+ request.data = args["data"];
|
|
|
|
|
+ request.user_id = ctx.user_id;
|
|
|
|
|
+
|
|
|
|
|
+ auto result = document_service_.CreateDocument(request);
|
|
|
|
|
+ if (!result.success) {
|
|
|
|
|
+ return {false, result.error, true, std::nullopt};
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ nlohmann::json response;
|
|
|
|
|
+ response["id"] = result.document->id;
|
|
|
|
|
+ response["collection"] = result.document->collection;
|
|
|
|
|
+ response["message"] = "Document created successfully";
|
|
|
|
|
+
|
|
|
|
|
+ return {true, response.dump(), false, std::nullopt};
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+auto ToolService::HandleUpdateDocument(const nlohmann::json& args,
|
|
|
|
|
+ const ToolExecutionContext& ctx) -> ToolExecutionResult {
|
|
|
|
|
+ if (!args.contains("collection") || !args["collection"].is_string()) {
|
|
|
|
|
+ return {false, "collection is required", true, std::nullopt};
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!args.contains("document_id") || !args["document_id"].is_string()) {
|
|
|
|
|
+ return {false, "document_id is required", true, std::nullopt};
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!args.contains("data") || !args["data"].is_object()) {
|
|
|
|
|
+ return {false, "data is required", true, std::nullopt};
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ std::string collection = args["collection"].get<std::string>();
|
|
|
|
|
+ std::string document_id = args["document_id"].get<std::string>();
|
|
|
|
|
+
|
|
|
|
|
+ // Get existing document to check ownership
|
|
|
|
|
+ auto existing = document_service_.GetDocument(ctx.workspace_id, collection, document_id);
|
|
|
|
|
+ if (!existing.success || !existing.document) {
|
|
|
|
|
+ return {false, "Document not found", true, std::nullopt};
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ std::string owner;
|
|
|
|
|
+ if (existing.document->data.contains("_created_by")) {
|
|
|
|
|
+ owner = existing.document->data["_created_by"].get<std::string>();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!authorization_service_.CanWriteDocument(ctx.user, ctx.workspace_id, collection, owner)) {
|
|
|
|
|
+ return {false, "Permission denied to update this document", true, std::nullopt};
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ UpdateDocumentRequest request;
|
|
|
|
|
+ request.workspace_id = ctx.workspace_id;
|
|
|
|
|
+ request.collection = collection;
|
|
|
|
|
+ request.id = document_id;
|
|
|
|
|
+ request.data = args["data"];
|
|
|
|
|
+ request.merge = true;
|
|
|
|
|
+ request.user_id = ctx.user_id;
|
|
|
|
|
+
|
|
|
|
|
+ auto result = document_service_.UpdateDocument(request);
|
|
|
|
|
+ if (!result.success) {
|
|
|
|
|
+ return {false, result.error, true, std::nullopt};
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return {true, "Document updated successfully", false, std::nullopt};
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+auto ToolService::HandleGetDocument(const nlohmann::json& args,
|
|
|
|
|
+ const ToolExecutionContext& ctx) -> ToolExecutionResult {
|
|
|
|
|
+ if (!args.contains("collection") || !args["collection"].is_string()) {
|
|
|
|
|
+ return {false, "collection is required", true, std::nullopt};
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!args.contains("document_id") || !args["document_id"].is_string()) {
|
|
|
|
|
+ return {false, "document_id is required", true, std::nullopt};
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ std::string collection = args["collection"].get<std::string>();
|
|
|
|
|
+ std::string document_id = args["document_id"].get<std::string>();
|
|
|
|
|
+
|
|
|
|
|
+ auto result = document_service_.GetDocument(ctx.workspace_id, collection, document_id);
|
|
|
|
|
+ if (!result.success || !result.document) {
|
|
|
|
|
+ return {false, result.error.empty() ? "Document not found" : result.error, true, std::nullopt};
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ std::string owner;
|
|
|
|
|
+ if (result.document->data.contains("_created_by")) {
|
|
|
|
|
+ owner = result.document->data["_created_by"].get<std::string>();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!authorization_service_.CanReadDocument(ctx.user, ctx.workspace_id, collection, owner)) {
|
|
|
|
|
+ return {false, "Permission denied to read this document", true, std::nullopt};
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Filter fields based on permissions
|
|
|
|
|
+ authorization_service_.FilterDocumentFields(ctx.user, ctx.workspace_id, collection,
|
|
|
|
|
+ result.document->data);
|
|
|
|
|
+
|
|
|
|
|
+ nlohmann::json response;
|
|
|
|
|
+ response["id"] = result.document->id;
|
|
|
|
|
+ response["collection"] = result.document->collection;
|
|
|
|
|
+ response["data"] = result.document->data;
|
|
|
|
|
+ response["created_at"] = result.document->created_at;
|
|
|
|
|
+ response["updated_at"] = result.document->updated_at;
|
|
|
|
|
+
|
|
|
|
|
+ return {true, response.dump(), false, std::nullopt};
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+auto ToolService::HandleListDocuments(const nlohmann::json& args,
|
|
|
|
|
+ const ToolExecutionContext& ctx) -> ToolExecutionResult {
|
|
|
|
|
+ if (!args.contains("collection") || !args["collection"].is_string()) {
|
|
|
|
|
+ return {false, "collection is required", true, std::nullopt};
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ std::string collection = args["collection"].get<std::string>();
|
|
|
|
|
+
|
|
|
|
|
+ // Check read permission and determine owner filter
|
|
|
|
|
+ DocumentQuery query;
|
|
|
|
|
+ query.workspace_id = ctx.workspace_id;
|
|
|
|
|
+ query.collection = collection;
|
|
|
|
|
+
|
|
|
|
|
+ // If user only has read_own, filter to their documents
|
|
|
|
|
+ if (!authorization_service_.CanReadAllDocuments(ctx.user, ctx.workspace_id, collection)) {
|
|
|
|
|
+ if (!authorization_service_.CanReadCollection(ctx.user, ctx.workspace_id, collection)) {
|
|
|
|
|
+ return {false, "Permission denied to read collection: " + collection, true, std::nullopt};
|
|
|
|
|
+ }
|
|
|
|
|
+ query.owner_filter = ctx.user_id;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (args.contains("filter") && args["filter"].is_object()) {
|
|
|
|
|
+ query.filter = args["filter"];
|
|
|
|
|
+ }
|
|
|
|
|
+ if (args.contains("sort_field") && args["sort_field"].is_string()) {
|
|
|
|
|
+ query.sort_field = args["sort_field"].get<std::string>();
|
|
|
|
|
+ }
|
|
|
|
|
+ if (args.contains("sort_ascending") && args["sort_ascending"].is_boolean()) {
|
|
|
|
|
+ query.sort_ascending = args["sort_ascending"].get<bool>();
|
|
|
|
|
+ }
|
|
|
|
|
+ if (args.contains("limit") && args["limit"].is_number_integer()) {
|
|
|
|
|
+ query.limit = args["limit"].get<int32_t>();
|
|
|
|
|
+ if (query.limit > 100) query.limit = 100; // Cap at 100
|
|
|
|
|
+ } else {
|
|
|
|
|
+ query.limit = 20; // Default limit
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ auto result = document_service_.ListDocuments(query);
|
|
|
|
|
+ if (!result.success) {
|
|
|
|
|
+ return {false, result.error, true, std::nullopt};
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ nlohmann::json response = nlohmann::json::array();
|
|
|
|
|
+ for (auto& doc : result.documents) {
|
|
|
|
|
+ // Filter fields for each document
|
|
|
|
|
+ authorization_service_.FilterDocumentFields(ctx.user, ctx.workspace_id, collection, doc.data);
|
|
|
|
|
+
|
|
|
|
|
+ response.push_back({
|
|
|
|
|
+ {"id", doc.id},
|
|
|
|
|
+ {"data", doc.data}
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return {true, response.dump(), false, std::nullopt};
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+auto ToolService::HandleDeleteDocument(const nlohmann::json& args,
|
|
|
|
|
+ const ToolExecutionContext& ctx) -> ToolExecutionResult {
|
|
|
|
|
+ if (!args.contains("collection") || !args["collection"].is_string()) {
|
|
|
|
|
+ return {false, "collection is required", true, std::nullopt};
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!args.contains("document_id") || !args["document_id"].is_string()) {
|
|
|
|
|
+ return {false, "document_id is required", true, std::nullopt};
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ std::string collection = args["collection"].get<std::string>();
|
|
|
|
|
+ std::string document_id = args["document_id"].get<std::string>();
|
|
|
|
|
+
|
|
|
|
|
+ // Get existing document to check ownership
|
|
|
|
|
+ auto existing = document_service_.GetDocument(ctx.workspace_id, collection, document_id);
|
|
|
|
|
+ if (!existing.success || !existing.document) {
|
|
|
|
|
+ return {false, "Document not found", true, std::nullopt};
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ std::string owner;
|
|
|
|
|
+ if (existing.document->data.contains("_created_by")) {
|
|
|
|
|
+ owner = existing.document->data["_created_by"].get<std::string>();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!authorization_service_.CanDeleteDocument(ctx.user, ctx.workspace_id, collection, owner)) {
|
|
|
|
|
+ return {false, "Permission denied to delete this document", true, std::nullopt};
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ auto result = document_service_.DeleteDocument(ctx.workspace_id, collection, document_id);
|
|
|
|
|
+ if (!result.success) {
|
|
|
|
|
+ return {false, result.error, true, std::nullopt};
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return {true, "Document deleted successfully", false, std::nullopt};
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+} // namespace smartbotic::webserver
|