| """ |
| Productivity Tool - Notion Integration |
| |
| Provides Notion workspace operations with governance integration. |
| |
| Supports: |
| - Workspace search (pages, databases) |
| - Database querying and schema inspection |
| - Page creation, editing, and content appending |
| - Read operations for INTERN+ maturity |
| - Write operations for SUPERVISED+ maturity |
| |
| Governance: |
| - Read actions (search, query, get): INTERN+ maturity |
| - Write actions (create, update, append): SUPERVISED+ maturity |
| - STUDENT agents blocked from all Notion operations |
| - Local-only mode enforcement (Notion requires cloud API) |
| """ |
|
|
| import asyncio |
| import json |
| from typing import Any, Dict, List, Optional, Tuple |
|
|
| from core.database import get_db_session |
| from core.governance_cache import GovernanceCache |
| from core.models import AgentRegistry |
| from core.productivity.notion_service import NotionService |
| from core.privsec.local_only_guard import LocalOnlyGuard |
| from core.structured_logger import get_logger |
|
|
| logger = get_logger(__name__) |
|
|
| |
| _governance_cache = GovernanceCache() |
|
|
|
|
| class NotionTool: |
| """ |
| Notion workspace tool for AI agents. |
| |
| Provides access to Notion workspaces for personal productivity: |
| - Search pages and databases |
| - Query databases with filters |
| - Create and edit pages |
| - Add content to pages |
| |
| **Governance**: |
| - Read operations (search, query, get): INTERN+ maturity |
| - Write operations (create, update, append): SUPERVISED+ maturity |
| - STUDENT agents blocked from all operations |
| |
| **Local-Only Mode**: |
| - Notion requires cloud API access |
| - Blocked in local-only mode (ATOM_LOCAL_ONLY=true) |
| - Suggests local alternatives (markdown files) |
| |
| Examples: |
| - "Search for pages about project X" |
| - "Show me tasks due this week" |
| - "Create a new task for tomorrow's meeting" |
| - "Mark the meeting task as complete" |
| - "Add meeting notes to the project database" |
| """ |
|
|
| def __init__(self): |
| """Initialize Notion tool with governance cache.""" |
| |
| self.governance_cache = GovernanceCache() |
|
|
| logger.info("NotionTool initialized", tool_available=True) |
|
|
| async def run( |
| self, |
| action: str, |
| agent_id: Optional[str] = None, |
| maturity_level: Optional[str] = None, |
| user_id: Optional[str] = None, |
| **kwargs |
| ) -> str: |
| """ |
| Execute Notion operation with governance enforcement. |
| |
| Args: |
| action: Operation to perform (search, list_databases, etc.) |
| agent_id: Agent identifier for governance check |
| user_id: User ID for token lookup |
| **kwargs: Action-specific parameters |
| |
| Returns: |
| Dict with operation results |
| |
| Raises: |
| PermissionError: If maturity level too low |
| RuntimeError: If Notion not connected or local-only mode enabled |
| """ |
| |
| read_actions = { |
| "search", |
| "list_databases", |
| "query_database", |
| "get_schema", |
| "get_page", |
| "get_blocks" |
| } |
|
|
| write_actions = { |
| "create_page", |
| "update_page", |
| "append_blocks", |
| } |
|
|
| |
| if action in read_actions: |
| required_maturity = "INTERN" |
| elif action in write_actions: |
| required_maturity = "SUPERVISED" |
| else: |
| return { |
| "success": False, |
| "error": f"Unknown action: {action}", |
| "available_actions": sorted(read_actions | write_actions) |
| } |
|
|
| |
| allowed, reason = await self._check_notion_permission( |
| agent_id=agent_id, |
| user_id=user_id or "default", |
| action=action, |
| required_maturity=required_maturity |
| ) |
|
|
| if not allowed: |
| logger.warning( |
| "Notion permission denied", |
| action=action, |
| agent_id=agent_id, |
| reason=reason |
| ) |
| raise PermissionError(reason) |
|
|
| |
| try: |
| result = await self._execute_action( |
| action=action, |
| user_id=user_id or "default", |
| **kwargs |
| ) |
|
|
| logger.info( |
| "Notion action completed", |
| action=action, |
| agent_id=agent_id |
| ) |
|
|
| return result |
|
|
| except PermissionError: |
| |
| raise |
| except Exception as e: |
| logger.error( |
| "Notion action failed", |
| action=action, |
| error=str(e), |
| kwargs=kwargs |
| ) |
| return { |
| "success": False, |
| "error": str(e), |
| "action": action |
| } |
|
|
| async def _check_notion_permission( |
| self, |
| agent_id: Optional[str], |
| user_id: str, |
| action: str, |
| required_maturity: str |
| ) -> Tuple[bool, Optional[str]]: |
| """ |
| Check if agent has permission for Notion operation. |
| |
| Args: |
| agent_id: Agent ID (None if human-triggered) |
| user_id: User ID |
| action: Action being performed |
| required_maturity: Required maturity level (INTERN or SUPERVISED) |
| |
| Returns: |
| (allowed, reason) tuple |
| """ |
| |
| if not agent_id: |
| return True, None |
|
|
| |
| cache_key = f"notion_{action}" |
| cached = _governance_cache.get(agent_id, cache_key) |
| if cached: |
| return cached.get("allowed", False), cached.get("reason") |
|
|
| |
| try: |
| with get_db_session() as db: |
| agent = db.query(AgentRegistry).filter( |
| AgentRegistry.id == agent_id |
| ).first() |
|
|
| if not agent: |
| return False, f"Agent '{agent_id}' not found" |
|
|
| |
| maturity = agent.maturity_level |
| maturity_order = ["STUDENT", "INTERN", "SUPERVISED", "AUTONOMOUS"] |
|
|
| try: |
| current_level = maturity_order.index(maturity) |
| required_level = maturity_order.index(required_maturity) |
| except ValueError: |
| return False, f"Invalid maturity level: {maturity}" |
|
|
| allowed = current_level >= required_level |
| reason = None |
|
|
| if not allowed: |
| reason = ( |
| f"Notion {action} requires {required_maturity}+ maturity " |
| f"(agent is {maturity})" |
| ) |
|
|
| |
| _governance_cache.set(agent_id, cache_key, { |
| "allowed": allowed, |
| "reason": reason, |
| "maturity": maturity |
| }) |
|
|
| |
| if allowed: |
| try: |
| guard = LocalOnlyGuard() |
| guard.allow_external_request( |
| service="notion", |
| reason=f"Notion API requires cloud access" |
| ) |
| except Exception as e: |
| return False, str(e) |
|
|
| return allowed, reason |
|
|
| except Exception as e: |
| logger.error("Permission check failed", error=str(e)) |
| return False, f"Permission check failed: {str(e)}" |
|
|
| async def _execute_action( |
| self, |
| action: str, |
| user_id: str, |
| **kwargs |
| ) -> Dict[str, Any]: |
| """ |
| Execute specific Notion action. |
| |
| Args: |
| action: Action to execute |
| user_id: User ID for Notion service |
| **kwargs: Action-specific parameters |
| |
| Returns: |
| Dict with action results |
| """ |
| |
| service = NotionService(user_id) |
|
|
| |
| if action == "search": |
| query = kwargs.get("query", "") |
| if not query: |
| return {"success": False, "error": "Query parameter required for search"} |
|
|
| results = await service.search_workspace(query) |
| return { |
| "success": True, |
| "action": "search", |
| "query": query, |
| "count": len(results), |
| "results": results |
| } |
|
|
| elif action == "list_databases": |
| databases = await service.list_databases() |
| return { |
| "success": True, |
| "action": "list_databases", |
| "count": len(databases), |
| "databases": databases |
| } |
|
|
| elif action == "query_database": |
| database_id = kwargs.get("database_id") |
| if not database_id: |
| return {"success": False, "error": "database_id parameter required"} |
|
|
| |
| filter_param = kwargs.get("filter") |
| if filter_param: |
| if isinstance(filter_param, str): |
| try: |
| filter_param = json.loads(filter_param) |
| except json.JSONDecodeError: |
| return {"success": False, "error": "Invalid filter JSON"} |
|
|
| pages = await service.query_database(database_id, filter_param) |
| return { |
| "success": True, |
| "action": "query_database", |
| "database_id": database_id, |
| "count": len(pages), |
| "pages": pages |
| } |
|
|
| elif action == "get_schema": |
| database_id = kwargs.get("database_id") |
| if not database_id: |
| return {"success": False, "error": "database_id parameter required"} |
|
|
| schema = await service.get_database_schema(database_id) |
| return { |
| "success": True, |
| "action": "get_schema", |
| "database_id": database_id, |
| "schema": schema |
| } |
|
|
| elif action == "get_page": |
| page_id = kwargs.get("page_id") |
| if not page_id: |
| return {"success": False, "error": "page_id parameter required"} |
|
|
| page = await service.get_page(page_id) |
| return { |
| "success": True, |
| "action": "get_page", |
| "page_id": page_id, |
| "page": page |
| } |
|
|
| elif action == "get_blocks": |
| page_id = kwargs.get("page_id") |
| if not page_id: |
| return {"success": False, "error": "page_id parameter required"} |
|
|
| blocks = await service.get_page_blocks(page_id) |
| return { |
| "success": True, |
| "action": "get_blocks", |
| "page_id": page_id, |
| "count": len(blocks), |
| "blocks": blocks |
| } |
|
|
| elif action == "create_page": |
| database_id = kwargs.get("database_id") |
| properties = kwargs.get("properties") |
|
|
| if not database_id: |
| return {"success": False, "error": "database_id parameter required"} |
| if not properties: |
| return {"success": False, "error": "properties parameter required"} |
|
|
| |
| if isinstance(properties, str): |
| try: |
| properties = json.loads(properties) |
| except json.JSONDecodeError: |
| return {"success": False, "error": "Invalid properties JSON"} |
|
|
| page = await service.create_page(database_id, properties) |
| return { |
| "success": True, |
| "action": "create_page", |
| "database_id": database_id, |
| "page": page |
| } |
|
|
| elif action == "update_page": |
| page_id = kwargs.get("page_id") |
| properties = kwargs.get("properties") |
|
|
| if not page_id: |
| return {"success": False, "error": "page_id parameter required"} |
| if not properties: |
| return {"success": False, "error": "properties parameter required"} |
|
|
| |
| if isinstance(properties, str): |
| try: |
| properties = json.loads(properties) |
| except json.JSONDecodeError: |
| return {"success": False, "error": "Invalid properties JSON"} |
|
|
| page = await service.update_page(page_id, properties) |
| return { |
| "success": True, |
| "action": "update_page", |
| "page_id": page_id, |
| "page": page |
| } |
|
|
| elif action == "append_blocks": |
| page_id = kwargs.get("page_id") |
| blocks = kwargs.get("blocks") |
|
|
| if not page_id: |
| return {"success": False, "error": "page_id parameter required"} |
| if not blocks: |
| return {"success": False, "error": "blocks parameter required"} |
|
|
| |
| if isinstance(blocks, str): |
| try: |
| blocks = json.loads(blocks) |
| except json.JSONDecodeError: |
| return {"success": False, "error": "Invalid blocks JSON"} |
|
|
| result = await service.append_page_blocks(page_id, blocks) |
| return { |
| "success": True, |
| "action": "append_blocks", |
| "page_id": page_id, |
| "result": result |
| } |
|
|
| else: |
| return { |
| "success": False, |
| "error": f"Unknown action: {action}", |
| "available_actions": sorted({ |
| "search", "list_databases", "query_database", "get_schema", |
| "get_page", "get_blocks", "create_page", "update_page", "append_blocks" |
| }) |
| } |
|
|
|
|
| |
|
|
| def register_notion_tool(tool_registry=None): |
| """ |
| Register NotionTool with tool registry. |
| |
| Args: |
| tool_registry: ToolRegistry instance (optional) |
| |
| Returns: |
| Registered NotionTool instance |
| """ |
| from tools.registry import ToolRegistry, get_tool_registry |
|
|
| if tool_registry is None: |
| tool_registry = get_tool_registry() |
|
|
| notion_tool = NotionTool() |
|
|
| tool_registry.register( |
| name="notion_tool", |
| function=notion_tool.run, |
| version="1.0.0", |
| description="Notion workspace operations for AI agents. Provides searching, querying, reading, and page creation.", |
| category="productivity", |
| complexity=3, |
| maturity_required="INTERN", |
| dependencies=["notion-client"], |
| tags=["notion", "knowledge", "database", "tasks", "notes", "workspace"] |
| ) |
|
|
| logger.info("NotionTool registered with ToolRegistry") |
|
|
| return notion_tool |
|
|