Notion API + AI агенты: автоматизация баз знаний — QantCore Guide
📝

Notion API + AI агенты: автоматизация баз знаний

Научите вашего AI-агента читать Notion-страницы, создавать документы, управлять базами данных и обновлять контент в реальном времени. Полный гайд по интеграции Notion API с Python-агентами: от получения API-ключа до автономного AI-ассистента, который сам ведёт вашу документацию, трекает задачи и обогащает знания из внешних источников.

intermediate ⏱ 15 мин 🐍 Python
Notion API + AI Agent — Knowledge Automation Pipeline 👤 Пользователь Промпт / Команда «Создай отчёт в Notion» 🤖 AI Agent Core LLM Router + Tool Selection Function Calling → Notion Tools Python + openai + notion-client 🧠 LLM (GPT-4o / Claude) 🔧 Tool Registry 📄 Notion API Поиск Чтение Создание Обновление Удаление Блоки 🗂️ Workspace Pages / DBs 🔧 Notion Tools (Function Defs) • search_notion(query) → Pages • get_page(page_id) → Content • create_page(parent, title, content) • update_page(page_id, blocks) • query_database(db_id, filter) 🔄 Knowledge Flow User Prompt → Intent Detection → Tool Selection → Notion API Call → Response Parsing → LLM Summary → Final Answer to User 2-way sync: Read ↔ Write 🎯 Use Cases 📋 Авто-документирование 🔍 RAG-поиск по базе знаний 📊 Авто-отчёты и дашборды ✅ Умный трекер задач 📝 Авто-релиз ноуты Notion API v1 · notion-client 2.x · OpenAI Function Calling · Python 3.11+

1 Получение Notion API ключа и настройка интеграции

Первым делом нужно создать интеграцию в Notion и получить API-ключ. Без этого шага AI-агент не сможет взаимодействовать с вашим workspace.

  1. Перейдите на notion.so/my-integrations и нажмите «New integration».
  2. Дайте интеграции имя (например, «AI Agent») и выберите workspace.
  3. В разделе Capabilities отметьте: Read content, Update content, Insert content. Для агента, который будет создавать и редактировать страницы, нужны все три.
  4. Скопируйте Internal Integration Secret — это ваш API-ключ (начинается с secret_ или ntn_).
  5. Критически важно: для каждой страницы или базы данных, к которой агент должен иметь доступ, нажмите «•••» → «Connections» → выберите вашу интеграцию. Без этого API будет возвращать 404.
# .env или прямо в коде
NOTION_API_KEY = "secret_abc123..."          # Ваш Internal Integration Secret
NOTION_DATABASE_ID = "c4f8e2a1..."      # ID базы данных (из URL)
NOTION_VERSION = "2022-06-28"            # Версия API (обязательный заголовок)

2 Установка и базовая настройка Python SDK

Notion предоставляет официальный Python SDK — notion-client. Установим его вместе с openai для LLM и python-dotenv для управления переменными окружения.

# Установка зависимостей
pip install notion-client openai python-dotenv rich
# config.py — базовая конфигурация агента
import os
from dotenv import load_dotenv
from notion_client import Client
from openai import OpenAI

load_dotenv()

# Notion client
notion = Client(auth=os.environ["NOTION_API_KEY"])
database_id = os.environ["NOTION_DATABASE_ID"]

# OpenAI client
openai_client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])

# Проверка подключения к Notion
try:
    me = notion.users.me()
    print(f"✅ Подключено как: {me['name']} ({me['type']})")
except Exception as e:
    print(f"❌ Ошибка подключения: {e}")
    raise

3 Реализация Notion Tool Functions для AI-агента

AI-агент использует function calling (tool use) — мы описываем функции, а LLM решает, какую вызвать и с какими аргументами. Ниже — полный набор инструментов для работы с Notion. Каждая функция возвращает структурированный JSON, который LLM может интерпретировать и пересказать пользователю.

# notion_tools.py — инструменты для AI-агента
from notion_client import Client
import json

notion = Client(auth="secret_...")

# ═══ 1. Поиск страниц и баз данных ═══
def search_notion(query: str, filter_type: str = "page") -> str:
    """Поиск в Notion по ключевым словам. filter_type: page или database."""
    results = notion.search(
        query=query,
        filter={"property": "object", "value": filter_type}
    ).get("results", [])

    if not results:
        return json.dumps({"found": 0, "message": "Ничего не найдено"})

    items = []
    for r in results[:10]:
        title = ""
        if "title" in r and r["title"]:
            title = " ".join([t.get("plain_text", "") for t in r["title"]])
        items.append({
            "id": r["id"].replace("-", ""),
            "title": title,
            "url": r.get("url", ""),
            "type": r["object"]
        })
    return json.dumps({"found": len(items), "items": items}, ensure_ascii=False)


# ═══ 2. Чтение содержимого страницы ═══
def get_page_content(page_id: str) -> str:
    """Получить содержимое Notion-страницы: все блоки с текстом."""
    blocks = []
    has_more = True
    cursor = None

    while has_more:
        response = notion.blocks.children.list(
            block_id=page_id, page_size=100,
            start_cursor=cursor
        )
        blocks.extend(response.get("results", []))
        has_more = response.get("has_more", False)
        cursor = response.get("next_cursor")

    text_parts = []
    for block in blocks:
        block_type = block.get("type", "")
        content = block.get(block_type, {})
        rich_text = content.get("rich_text", [])
        line = "".join([t.get("plain_text", "") for t in rich_text])
        if line.strip():
            prefix = {"heading_1": "# ", "heading_2": "## ", "heading_3": "### "}.get(block_type, "")
            text_parts.append(prefix + line)

    full_text = "\n".join(text_parts)[:8000]  # Ограничиваем контекст
    return json.dumps({
        "page_id": page_id,
        "block_count": len(blocks),
        "content": full_text,
        "truncated": len("\n".join(text_parts)) > 8000
    }, ensure_ascii=False)


# ═══ 3. Создание новой страницы ═══
def create_notion_page(title: str, content: str, parent_page_id: str = None) -> str:
    """Создать страницу в Notion с markdown-подобным контентом."""
    parent = {}
    if parent_page_id:
        parent = {"type": "page_id", "page_id": parent_page_id}
    else:
        parent = {"type": "database_id", "database_id": database_id}

    new_page = notion.pages.create(
        parent=parent,
        properties={
            "title": {"title": [{"type": "text", "text": {"content": title}}]}
        },
        children=_text_to_blocks(content)
    )

    return json.dumps({
        "status": "created",
        "page_id": new_page["id"].replace("-", ""),
        "url": new_page.get("url", ""),
        "title": title
    }, ensure_ascii=False)


# ═══ 4. Обновление содержимого страницы ═══
def update_page_content(page_id: str, new_content: str, mode: str = "replace") -> str:
    """Обновить содержимое страницы. mode: replace — заменить всё, append — добавить в конец."""
    if mode == "replace":
        # Удаляем все существующие блоки
        existing = notion.blocks.children.list(block_id=page_id, page_size=100)
        for block in existing.get("results", []):
            notion.blocks.delete(block_id=block["id"])

    # Добавляем новые блоки
    notion.blocks.children.append(
        block_id=page_id,
        children=_text_to_blocks(new_content)
    )

    return json.dumps({
        "status": "updated",
        "page_id": page_id,
        "mode": mode
    })


# ═══ 5. Запрос к базе данных ═══
def query_database(db_id: str, status_filter: str = None, limit: int = 20) -> str:
    """Запросить записи из базы данных с опциональным фильтром по статусу."""
    filter_obj = {}
    if status_filter:
        filter_obj = {
            "property": "Status",
            "status": {"equals": status_filter}
        }

    response = notion.databases.query(
        database_id=db_id,
        filter=filter_obj if filter_obj else None,
        page_size=limit
    )

    rows = []
    for page in response.get("results", []):
        props = page.get("properties", {})
        title = ""
        for key, prop in props.items():
            if prop.get("type") == "title":
                title = "".join([t.get("plain_text", "") for t in prop.get("title", [])])
                break
        rows.append({
            "id": page["id"].replace("-", ""),
            "title": title,
            "url": page.get("url", "")
        })

    return json.dumps({
        "total": len(rows),
        "has_more": response.get("has_more", False),
        "rows": rows
    }, ensure_ascii=False)


# ═══ Вспомогательная функция: текст → Notion блоки ═══
def _text_to_blocks(text: str) -> list:
    """Конвертирует markdown-подобный текст в Notion block objects."""
    blocks = []
    for line in text.strip().split("\n"):
        if not line.strip():
            continue
        block_type = "paragraph"
        content = line
        if line.startswith("# "):
            block_type = "heading_1"; content = line[2:]
        elif line.startswith("## "):
            block_type = "heading_2"; content = line[3:]
        elif line.startswith("- "):
            block_type = "bulleted_list_item"; content = line[2:]
        blocks.append({
            "object": "block",
            "type": block_type,
            block_type: {"rich_text": [{"type": "text", "text": {"content": content}}]}
        })
    return blocks

4 Сборка AI-агента с Function Calling

Теперь собираем AI-агента, который использует OpenAI Function Calling для выбора и вызова Notion-инструментов. Агент принимает промпт пользователя на естественном языке, выбирает нужную функцию, вызывает Notion API и возвращает понятный ответ.

# agent.py — AI-агент с Notion tools
import json
from openai import OpenAI
from notion_tools import (
    search_notion, get_page_content, create_notion_page,
    update_page_content, query_database
)

client = OpenAI()

# ═══ Определение функций для Function Calling ═══
TOOLS = [
    {
        "type": "function",
        "function": {
            "name": "search_notion",
            "description": "Поиск страниц и баз данных в Notion по ключевым словам",
            "parameters": {
                "type": "object",
                "properties": {
                    "query": {"type": "string", "description": "Поисковый запрос"},
                    "filter_type": {"type": "string", "enum": ["page", "database"], "description": "Тип объекта"}
                },
                "required": ["query"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "get_page_content",
            "description": "Получить полное содержимое Notion-страницы по её ID",
            "parameters": {
                "type": "object",
                "properties": {
                    "page_id": {"type": "string", "description": "ID страницы Notion (32 символа, без дефисов)"}
                },
                "required": ["page_id"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "create_notion_page",
            "description": "Создать новую страницу в Notion с заданным заголовком и содержимым",
            "parameters": {
                "type": "object",
                "properties": {
                    "title": {"type": "string", "description": "Заголовок страницы"},
                    "content": {"type": "string", "description": "Содержимое (markdown): каждая строка = блок"},
                    "parent_page_id": {"type": "string", "description": "ID родительской страницы (опционально)"}
                },
                "required": ["title", "content"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "update_page_content",
            "description": "Обновить содержимое существующей страницы (заменить или дополнить)",
            "parameters": {
                "type": "object",
                "properties": {
                    "page_id": {"type": "string", "description": "ID страницы"},
                    "new_content": {"type": "string", "description": "Новое содержимое (markdown)"},
                    "mode": {"type": "string", "enum": ["replace", "append"], "description": "Режим обновления"}
                },
                "required": ["page_id", "new_content"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "query_database",
            "description": "Запросить записи из базы данных Notion с фильтрацией",
            "parameters": {
                "type": "object",
                "properties": {
                    "db_id": {"type": "string", "description": "ID базы данных"},
                    "status_filter": {"type": "string", "description": "Фильтр по статусу (например: In Progress, Done)"},
                    "limit": {"type": "integer", "description": "Максимальное количество записей"}
                },
                "required": ["db_id"]
            }
        }
    }
]

# ═══ Маппинг функций ═══
AVAILABLE_FUNCTIONS = {
    "search_notion": search_notion,
    "get_page_content": get_page_content,
    "create_notion_page": create_notion_page,
    "update_page_content": update_page_content,
    "query_database": query_database,
}


# ═══ Основной цикл агента ═══
def run_agent(user_prompt: str) -> str:
    """Запустить AI-агента с пользовательским промптом."""
    messages = [
        {"role": "system", "content": """Ты AI-ассистент с доступом к Notion workspace пользователя.
Ты можешь: искать страницы, читать содержимое, создавать новые документы,
обновлять существующие, и запрашивать базы данных.
Отвечай на русском. Всегда указывай URL созданных/найденных страниц."""},
        {"role": "user", "content": user_prompt}
    ]

    # Первый вызов: LLM решает, нужен ли tool call
    response = client.chat.completions.create(
        model="gpt-4o",
        messages=messages,
        tools=TOOLS,
        tool_choice="auto",
        temperature=0.3
    )

    response_message = response.choices[0].message
    tool_calls = response_message.tool_calls

    # Если tool call не нужен — возвращаем текстовый ответ
    if not tool_calls:
        return response_message.content

    # Добавляем ответ ассистента с tool_calls в историю
    messages.append(response_message)

    # Выполняем все tool calls
    for tool_call in tool_calls:
        function_name = tool_call.function.name
        function_args = json.loads(tool_call.function.arguments)

        print(f"🔧 Вызов: {function_name}({function_args})")

        function_to_call = AVAILABLE_FUNCTIONS[function_name]
        function_response = function_to_call(**function_args)

        # Добавляем результат tool call в историю
        messages.append({
            "role": "tool",
            "tool_call_id": tool_call.id,
            "content": function_response
        })

    # Второй вызов: LLM формулирует финальный ответ на основе результатов
    final_response = client.chat.completions.create(
        model="gpt-4o",
        messages=messages,
        temperature=0.5
    )

    return final_response.choices[0].message.content


# ═══ Пример использования ═══
if __name__ == "__main__":
    result = run_agent("Найди все страницы про API интеграции и создай сводный документ с их кратким содержанием")
    print(f"\n📄 Ответ агента:\n{result}")

5 Продвинутые сценарии: RAG-поиск по Notion и авто-документирование

Когда базовые инструменты готовы, можно строить сложные AI-пайплайны. Два самых востребованных сценария: RAG-поиск по всей базе знаний (агент читает Notion-страницы, индексирует их содержимое и отвечает на вопросы с цитированием источников) и авто-документирование (агент мониторит изменения в коде/задачах и автоматически обновляет документацию в Notion).

Сценарий A: RAG-поиск по Notion Knowledge Base

# rag_notion.py — RAG-поиск по Notion workspace
from notion_tools import search_notion, get_page_content
from openai import OpenAI
import json

client = OpenAI()

def rag_search_notion(question: str, top_k: int = 5) -> str:
    """RAG: найти релевантные страницы → прочитать → ответить с цитатами."""

    # Шаг 1: Поиск релевантных страниц
    search_result = json.loads(search_notion(query=question, filter_type="page"))
    if search_result["found"] == 0:
        return "❌ Не найдено релевантных страниц в Notion."

    # Шаг 2: Читаем содержимое top-K страниц
    contexts = []
    for item in search_result["items"][:top_k]:
        page_data = json.loads(get_page_content(item["id"]))
        contexts.append({
            "title": item["title"],
            "url": item["url"],
            "content": page_data.get("content", "")
        })

    # Шаг 3: Формируем промпт с контекстом и отправляем LLM
    context_text = "\n\n---\n\n".join([
        f"Источник: {c['title']} ({c['url']})\n{c['content'][:3000]}"
        for c in contexts
    ])

    rag_response = client.chat.completions.create(
        model="gpt-4o",
        messages=[
            {"role": "system", "content": "Отвечай на вопрос, используя ТОЛЬКО предоставленный контекст из Notion. Указывай источники (URL страниц). Если информации недостаточно — скажи об этом честно. Отвечай на русском."},
            {"role": "user", "content": f"Контекст из Notion:\n{context_text}\n\nВопрос: {question}"}
        ],
        temperature=0.2
    )
    return rag_response.choices[0].message.content

Сценарий B: Авто-документирование

# auto_docs.py — автоматическое обновление Notion-документации
from notion_tools import update_page_content, search_notion
import json

def auto_update_release_notes(version: str, changes: list):
    """Автоматически обновляет Release Notes в Notion с новыми изменениями."""

    # Ищем страницу Release Notes
    search = json.loads(search_notion(query="Release Notes", filter_type="page"))

    if search["found"] == 0:
        return "❌ Страница Release Notes не найдена. Создайте её вручную и подключите интеграцию."

    page_id = search["items"][0]["id"]

    # Формируем блок с изменениями
    new_section = f"## {version}\n\n"
    for change in changes:
        new_section += f"- {change}\n"

    # Добавляем в начало страницы (append)
    result = json.loads(update_page_content(
        page_id=page_id,
        new_content=new_section,
        mode="append"
    ))
    return f"✅ Release Notes обновлены: добавлена версия {version}"

# Пример использования
changes_list = [
    "Добавлен AI-агент для Notion",
    "Исправлен баг с пагинацией блоков",
    "Обновлён SDK до версии 2.3.0"
]
print(auto_update_release_notes("v2.3.0", changes_list))
# Вывод: ✅ Release Notes обновлены: добавлена версия v2.3.0

🎯 Итог: что мы построили

Мы создали полноценного AI-агента, который умеет работать с Notion API через function calling. Агент понимает промпты на естественном языке и автоматически выбирает нужный инструмент: поиск страниц, чтение содержимого, создание новых документов, обновление существующих и запросы к базам данных.

🔍
Поиск знаний

RAG-поиск по всему workspace с цитированием источников

📝
Авто-документы

Автоматическое создание и обновление страниц

📊
Умные базы

Фильтрация и анализ данных в Notion-базах

🔄
CI/CD доки

Авто-релиз ноуты из GitHub/GitLab в Notion

Ключевые выводы

  • Notion API v1 — зрелый и стабильный REST API с отличным Python SDK (notion-client). Документация полная, сообщество активное.
  • Function Calling — идеальный паттерн для AI-агентов: LLM выбирает инструмент, агент исполняет, результат возвращается пользователю на естественном языке.
  • Безопасность: никогда не хардкодьте API-ключи. Используйте .env + python-dotenv. Ограничивайте доступ интеграции только к нужным страницам через Notion Connections.
  • Пагинация: Notion API возвращает максимум 100 блоков/записей за запрос. Всегда обрабатывайте has_more и next_cursor для больших страниц.
  • Рейт-лимиты: 3 запроса в секунду для стандартной интеграции. Для продакшена используйте кеширование и очередь с backoff-стратегией.
  • RAG + Notion = 🔥: индексация Notion-страниц даёт AI-агенту доступ к корпоративной базе знаний. Комбинируйте с эмбеддингами для семантического поиска.

🚀 Следующий шаг

Разверните агента как Telegram-бота или Slack-приложение. Добавьте эмбеддинги для семантического поиска. Подключите вебхуки для real-time синхронизации.

📖 Notion API Reference

QantCore Guide • ai-agent-notion • Июнь 2026 • slug: ai-agent-notion