Перейти к содержанию

От консольного приложения к FastAPI

Шаг 1

За четыре модуля мы создали несколько полноценных AI-функций. Наше приложение умеет вести диалог с историей, вызывать разрешённые инструменты, анализировать обращения, обрабатывать документы, таблицы и изображения. Но пока пользователь может работать с этими возможностями только через консольные сценарии.

Для обучения такой интерфейс был удобен. Мы запускали Python-файл, вводили сообщение через input() и сразу видели результат в терминале. Консоль позволяла сосредоточиться на логике LLM-приложения, не отвлекаясь на устройство веб-сервера.

Однако веб-интерфейс, мобильное приложение или другой сервис не смогут удобно управлять нашим терминалом. Им нужен стабильный программный способ передать данные приложению и получить ответ. Поэтому в пятом модуле мы превратим существующий проект в AI-бэкенд — HTTP-сервис, который предоставляет созданные AI-функции через понятные адреса и контракты.

Что такое HTTP API?

HTTP API — это программный интерфейс, через который один клиент обращается к серверу по сети. Клиентом может быть браузер, фронтенд-приложение, мобильное приложение, Telegram-бот или другой бэкенд-сервис.

Вместо приглашения Вы: клиент отправляет HTTP-запрос. В запросе указываются адрес операции, HTTP-метод, заголовки и, если нужно, данные. Сервер принимает запрос, вызывает нужную прикладную логику и возвращает HTTP-ответ.

Например позднее клиент сможет отправить сообщение чата в JSON. FastAPI проверит форму запроса, получит нужный ChatService и передаст ему обычную Python-строку. После завершения работы результат сервиса будет преобразован в HTTP-ответ.

Здесь важно разделять два значения слова «клиент». LLMClient — наш Python-компонент для обращения к провайдеру модели. А HTTP-клиент — внешняя программа, которая обращается уже к нашему FastAPI-приложению.

И перед тем как переходить к практике, давайте обсудим, а что изменится при переходе от консоли?

Консоль обрабатывает действия последовательно внутри одного процесса. Она сама спрашивает текст через input(), печатает результат и ожидает следующую команду. HTTP-сервер устроен иначе: он постоянно запущен и принимает отдельные запросы от разных клиентов.

В HTTP-сценарии нельзя рассчитывать на то, что пользователь постоянно находится в одном терминальном цикле. Каждый запрос должен явно содержать необходимые данные: текст сообщения, идентификатор сессии, параметры файла или подтверждение действия. Позднее мы опишем эти данные Pydantic-моделями.

У каждого ответа также появятся HTTP-статус и JSON-тело. Успешный результат, неправильный ввод, отсутствие сессии и ошибка провайдера должны отличаться не случайным текстом в терминале, а стабильным контрактом. Этим займёмся в следующем разделе.

Но самое важное изменение связано с количеством пользователей. Пока один клиент ждёт ответ модели, сервер может получать другие запросы. Поэтому нам предстоит учитывать параллельную обработку, время жизни общих объектов, потоковую передачу и отключение клиента. Мы будем добавлять эти возможности постепенно, а не пытаться решить всё в первой лекции.

Но переход к FastAPI не означает, что нам нужно будет переписывать ChatService, SupportRequestAnalyzer, DocumentAIService или инструменты. Эти классы описывают прикладные сценарии и уже не зависят от консоли. Они получают обычные Python-объекты и возвращают обычные результаты.

Изменится только внешний слой приложения. Сейчас app/console.py превращает ввод пользователя в вызов сервиса и печатает ответ. А новый HTTP-слой будет получать запрос, преобразовывать его в аргументы того же сервиса, а затем сериализовать результат в JSON.

Таким образом, с приложением можно работать двумя способами и через консоль, и через FastAPI. При этом специально усложнять структуру проекта мы не будем. В нашем приложении уже есть понятное разделение ответственности между основными компонентами:

  • app/services продолжит хранить прикладные сценарии
  • app/llm, app/files и app/tools останутся интеграционными и инфраструктурными компонентами
  • app/application.py продолжит собирать связанные объекты приложения
  • а уже app/api будет отвечать за HTTP-запросы и ответы

А значит наш маршрут FastAPI будет тонким, то есть он будет только принимать HTTP-данные, получать готовый сервис, вызывать его и превращать результат или прикладную ошибку в HTTP-ответ. А уже промпты, работа с историей и правила инструментов остаются за пределами маршрута.

При этом всем, вы должны понимать, что в этом курсе мы пройдём FastAPI по самым верхам и разберём только те возможности, которые будут нам необходимы для создания нашего AI-бэкенда, а именно маршруты, Pydantic-модели запросов и ответов, зависимости, жизненный цикл приложения, обработку ошибок, загрузку файлов, потоковую передачу и тестирование API.

Мы не будем изучать весь фреймворк, проектировать большую информационную систему или подробно разбирать базы данных и сложную архитектуру. Наша цель только научиться правильно предоставлять уже созданные AI-функции через HTTP.

Если вы хотите изучить FastAPI подробнее, у меня есть отдельные курсы:

Давайте подведём итог. FastAPI станет новым внешним интерфейсом поверх уже написанной AI-логики, а не заменой этой логики. Консоль и HTTP API будут использовать одни и те же прикладные сервисы, поэтому промпты, история диалога, инструменты и обращения к модели не окажутся продублированы в маршрутах.

В следующем шаге мы перейдём к практике, а именно установим FastAPI в существующем окружении, создадим пакет app/api, добавим первый маршрут /health и впервые запустим проект как HTTP-сервер.

Шаг 2

Мы определили, что FastAPI будет внешним слоем приложения. Теперь создадим минимальный HTTP-сервис и убедимся, что сервер принимает запросы. AI-функции пока подключать не будем и сначала отдельно посмотрим на базовую механику маршрута, запуска и JSON-ответа.

И перед тем как переходить к написанию кода, давайте установим FastAPI, для этого в виртуальном окружении проекта выполните следующие команды:

pip install "fastapi[standard]"
pip freeze > requirements.txt

FastAPI использует Pydantic для работы с данными и Starlette для веб-возможностей, а кавычки вокруг fastapi[standard] особенно важны в терминалах, где квадратные скобки могут интерпретироваться оболочкой. Все способы установки и запуска можно дополнительно посмотреть в официальной документации FastAPI.

Создаём HTTP-слой

Внутри app создайте каталог api, а в нём каталог routes. В оба каталога добавьте пустые файлы __init__.py:

app/
├── api/
│   ├── __init__.py
│   ├── main.py
│   └── routes/
│       ├── __init__.py
│       └── health.py
├── files/
├── llm/
├── models/
├── prompts/
├── services/
├── tools/
├── __init__.py
├── application.py
├── chat_history.py
├── config.py
└── console.py

В этой структуре app/api/main.py станет точкой входа HTTP-приложения, при этом не путайте его с корневым main.py, и первый запускается веб-сервером, а второй по-прежнему можно запускать как обычный Python-файл для работы с консолью.

Каталог routes будет содержать маршруты API. Сейчас у нас появится только проверка состояния сервиса внутри health.py, но позднее рядом будут находиться маршруты чата, анализа обращений и документов. И благодаря этим отдельным файлам app/api/main.py не превратится в длинный список несвязанных функций.

Откройте новый файл app/api/routes/health.py и добавьте:

from fastapi import APIRouter

router = APIRouter(tags=["system"])

@router.get("/health")
async def health() -> dict[str, str]:
    return {"status": "ok"}

APIRouter позволяет объединять связанные HTTP-маршруты в отдельном файле. Например, в health.py мы будем хранить маршруты, предназначенные для проверки состояния приложения. Пока мы только создали объект router, поэтому FastAPI ещё не знает о маршрутах внутри него. Позже мы явно подключим этот объект к основному приложению с помощью метода include_router().

Декоратор @router.get("/health") связывает функцию health() с двумя характеристиками HTTP-запроса: методом GET и путём /health. Когда сервер получит именно такой запрос, FastAPI вызовет функцию под декоратором.

GET обычно используют для чтения состояния без изменения данных. Проверка здоровья ничего не создаёт и не изменяет, поэтому этот метод здесь подходит.

Обратите внимание, функция возвращает обычный Python-словарь, а уже FastAPI преобразует его в JSON и установит успешный HTTP-статус 200 OK. В следующих разделах мы заменим словари прикладных маршрутов на отдельные Pydantic-модели, но пока для простого технического ответа текущего типа достаточно.

Теперь откройте app/api/main.py и создайте само приложение:

from fastapi import FastAPI

from app.api.routes.health import router as health_router

def create_api() -> FastAPI:
    application = FastAPI(
        title="AI Support Desk API",
        version="1.0.0",
    )
    application.include_router(health_router)
    return application

app = create_api()

FastAPI — основной класс фреймворка. Объект application хранит конфигурацию HTTP-приложения, маршруты и настройки документации. Параметры title и version пока являются только метаданными API и не связаны с версией промпта или модели. А метод include_router() подключает маршруты из health.py. Без этой строки функция существовала бы в Python-файле, но FastAPI не добавил бы путь /health в приложение.

Функция create_api() собирает HTTP-слой и возвращает готовый объект. Сейчас она очень маленькая, но позднее здесь появятся другие маршруты и общие настройки. При этом конкретные обработчики по-прежнему останутся в своих файлах.

Последняя строка создаёт публичный объект app. Именно его найдёт команда запуска FastAPI. Создание объекта при импорте здесь допустимо, потому что пока оно не открывает сетевые соединения и не создаёт тяжёлые зависимости.

Если вы уже смотрели документацию FastAPI, то могли увидеть более простой способ создания приложения:

app = FastAPI(
    title="AI Support Desk API",
    version="1.0.0",
)
app.include_router(health_router)

Такой вариант полностью корректен и объект FastAPI создаётся прямо в модуле, после чего к нему подключаются маршруты. Для небольшого приложения этого обычно достаточно.

В нашем проекте мы используем функцию create_api(), которую часто называют фабрикой приложения. Она создаёт новый экземпляр FastAPI, подключает к нему маршруты и возвращает готовое приложение. Такой подход немного длиннее, но лучше подходит для растущего проекта. А позднее у нас появятся новые маршруты, жизненный цикл и автоматические тесты. Благодаря фабрике вся сборка HTTP-приложения останется в одном месте, а при тестировании мы сможем создавать отдельный экземпляр приложения для каждого сценария.

При этом вы должны понимать, что это не обязательное требование FastAPI, а выбранный нами способ организации кода. Оба варианта являются корректными и современными.

Запускаем первый HTTP-сервер

Из корня проекта выполните:

fastapi dev app/api/main.py

Команда fastapi dev запускает сервер разработки через Uvicorn. Он импортирует файл app/api/main.py, находит объект app и начинает слушать локальный адрес 127.0.0.1:8000.

(.venv) ~/PycharmProjects/AI_Course_Developer/ai-support-desk git:[master] fastapi dev app/api/main.py

 ⚡️ Starting FastAPI in development mode

 🐍 Using import string: app.api.main:app

 🌐 Server started at http://127.0.0.1:8000
    Documentation at http://127.0.0.1:8000/docs

  Logs:

 ▕  Will watch for changes in these directories: ['/Users/permin0ff/PycharmProjects/AI_Course_Developer/ai-support-desk']
 ▕  Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
 ▕  Started reloader process [94085] using WatchFiles
 ▕  Started server process [94091]
 ▕  Waiting for application startup.
 ▕  Application startup complete.

Режим разработки следит за изменениями Python-файлов и автоматически перезапускает сервер. Это удобно во время разработки, но режим fastapi dev не используют для запуска готового приложения, ведь он следит за файлами и автоматически перезапускает сервер после изменений. А еще пользователи Windows могут получить ситуацию, когда процесс Uvicorn зависает в фоне, и перестает видеть изменения в коде. Тогда для запуска без автоматической перезагрузки используйте команду:

fastapi run app/api/main.py

Команда fastapi run запускает Uvicorn в производственном режиме и по умолчанию принимает подключения на адресе 0.0.0.0:8000. Это базовый способ запуска приложения на сервере или внутри контейнера. В реальном проекте дополнительно настраивают HTTPS, переменные окружения, перезапуск процесса и мониторинг. Подробнее об этом можно прочитать в документации FastAPI.

Не закрывая терминал с сервером, откройте в браузере адрес:

http://127.0.0.1:8000/health

Вы должны получить:

Тот же запрос можно отправить из второго терминала:

curl http://127.0.0.1:8000/health

curl выступает HTTP-клиентом. Он обращается к серверу, а сервер вызывает health() и возвращает JSON. В отличие от запуска python main.py, новый процесс не спрашивает сообщения через input(). Он продолжает работать и ждёт сетевые запросы.

Также откройте:

http://127.0.0.1:8000/docs

FastAPI автоматически строит интерактивную документацию на основе подключённых маршрутов и их типов. Пока там находится только GET /health. И можете нажать Try it out и дальше кнопку Execute, которая и отправит запрос:

В следующем разделе появятся модели запросов и ответов, после чего документация станет значительно полезнее.

И подводя итоги данного шага, мы можем сказать что наш первый HTTP-маршрут работает, но приложение ещё не создаёт LLMClient, ChatService и остальные общие объекты. Если начать создавать их внутри каждого будущего маршрута, ресурсы будут повторно открываться при каждом запросе. В следующем шаге мы решим эту проблему с помощью жизненного цикла FastAPI.

Шаг 3

Минимальный HTTP-сервер уже отвечает на GET /health. Пока это работает без общих зависимостей, а именно функция возвращает готовый словарь и не обращается к AI-сервисам. Но будущему маршруту чата понадобится ChatService, а анализу обращения — SupportRequestAnalyzer.

Сейчас консоль решает эту задачу в main.py. Сначала создаются Settings и контейнер приложения, затем запускается run_console(), а в блоке finally вызывается application.close(). Благодаря этому LLMClient и пул потоков ToolExecutor освобождаются даже при ошибке или остановке программы.

HTTP-серверу нужна та же гарантия, но его время работы устроено иначе. Он должен один раз создать общие зависимости перед приёмом запросов, использовать их во многих запросах, а при остановке один раз закрыть.

Почему нельзя создавать сервисы в маршруте?

Представим, что будущий маршрут чата при каждом вызове выполняет Settings() и build_application(). Тогда для каждого сообщения будет создаваться новый HTTP-клиент провайдера, новый реестр инструментов, новый ToolExecutor с пулом потоков и новый ChatService.

Это не только лишняя работа. Новый ChatService не будет знать историю предыдущего запроса, а созданные ресурсы придётся аккуратно закрывать после каждого ответа. Состояние приложения станет зависеть от кода отдельного маршрута.

Нам нужна управляемая последовательность:

Чтобы выполнить подготовку один раз перед началом работы сервера, а при его остановке корректно освободить созданные ресурсы, в FastAPI предусмотрен специальный механизм — lifespan, или жизненный цикл приложения. Рассмотрим, как он работает и как поможет связать HTTP-сервер с уже существующей сборкой нашего проекта.

Что такое lifespan?

Lifespan — это управляемый жизненный цикл FastAPI-приложения. Код до yield выполняется один раз перед тем, как сервер начнёт принимать запросы. Код после yield выполняется один раз при корректной остановке сервера.

Это подходящее место для ресурсов, которые принадлежат всему процессу: LLM-клиента, пула соединений с базой данных, реестра инструментов или загруженной модели. В нашем проекте необходимые объекты уже собирает функция build_application(), поэтому дублировать их создание в HTTP-слое не придётся.

Внутри app/api создайте файл lifespan.py:

from collections.abc import AsyncIterator
from contextlib import asynccontextmanager

from fastapi import FastAPI

from app.application import build_application
from app.config import Settings

@asynccontextmanager
async def lifespan(
    application: FastAPI,
) -> AsyncIterator[None]:
    settings = Settings()
    container = build_application(settings)
    application.state.container = container

    try:
        yield
    finally:
        container.close()

Декоратор @asynccontextmanager превращает функцию с yield в асинхронный контекстный менеджер. Сам FastAPI войдёт в этот контекст при запуске и выйдет из него при остановке. Вручную вызывать lifespan() не нужно.

Сначала создаётся Settings. Конфигурация читается один раз для текущего процесса. Если обязательного LLM_API_KEY нет или значение другого параметра не проходит Pydantic-валидацию, запуск остановится до приёма запросов. Это правильнее, чем запустить неработоспособный сервер и обнаружить ошибку только при первом обращении пользователя.

Затем вызывается уже знакомая функция build_application(settings). Внутри неё создаются один LLMClient, реестр инструментов, ToolExecutor, BoundedToolLoop, политика истории, сервис суммаризации, ChatService и SupportRequestAnalyzer.

Получившийся ApplicationContainer сохраняется в application.state. Это состояние принадлежит конкретному экземпляру FastAPI и будет доступно будущим маршрутам. Мы не создаём глобальную переменную с контейнером и не собираем сервисы повторно.

В следующем разделе появится небольшая FastAPI-зависимость, которая безопасно получит контейнер из application.state и передаст нужный сервис маршруту. Пока напрямую обращаться к этому полю из health() не требуется.

После подготовки выполняется yield. В этот момент часть запуска закончена и сервер может обрабатывать запросы.

Когда сервер получает сигнал завершения, выполнение продолжается внутри finally. Метод container.close() сначала корректно завершает пул потоков ToolExecutor, а затем закрывает внутренний HTTP-клиент LLMClient. Блок finally сохраняет эту гарантию и при ошибке во время остановки.

Теперь подключим жизненный цикл к приложению. Откройте app/api/main.py и добавьте импорт:

from app.api.lifespan import lifespan

Затем передайте его при создании FastAPI:

def create_api() -> FastAPI:
    application = FastAPI(
        title="AI Support Desk API",
        version="1.0.0",
        lifespan=lifespan,
    )
    application.include_router(health_router)
    return application

Остальной код файла не меняется и объект app по-прежнему создаётся через create_api(), но ресурсы не открываются во время импорта. Их сборка начнётся только тогда, когда сервер запустит lifespan.

То есть корневой файл main.py по-прежнему остаётся точкой входа для консольного приложения, поэтому изменять его не нужно. При запуске консоли он вызывает build_application(), получает контейнер со всеми сервисами, передаёт их в run_console(), а после завершения работы закрывает контейнер в блоке finally.

А у FastAPI будет своя точка входа app/api/main.py. Она использует ту же функцию build_application(), поэтому не создаёт отдельные копии правил сборки сервисов. Отличается только управление временем жизни контейнера и в консоли за его создание и закрытие отвечает корневой main.py, а в HTTP-приложении эти действия выполняет механизм lifespan при запуске и остановке сервера.

Так HTTP-слой не знает подробностей сборки реестра, истории и инструментов. Он только управляет началом и завершением жизни готового контейнера.

В следующем шаге мы снова запустим сервер и проверим обе половины lifespan. Разберём, что именно подтверждает /health, почему этот маршрут не должен обращаться к ProxyAPI и что происходит при автоматической перезагрузке или остановке процесса.

Шаг 4

Теперь HTTP-приложение состоит не только из маршрута. Перед началом работы lifespan читает конфигурацию, собирает существующие AI-компоненты и сохраняет контейнер в состоянии FastAPI. При остановке тот же механизм освобождает ресурсы.

В этом шаге мы не будем добавлять новые классы, а только запустим готовую последовательность и разберём её по наблюдаемым этапам. Это важно сделать до появления маршрутов чата и анализа документов, ведь если базовый жизненный цикл работает неправильно, то ошибки всех будущих маршрутов будут выглядеть одинаково.

Давайте еще раз запустим наше приложение:

fastapi dev app/api/main.py

При запуске FastAPI импортирует объект app, входит в lifespan и вызывает Settings(). После успешной конфигурации выполняется build_application(). Только когда эта часть завершится, Uvicorn сообщит, что запуск приложения закончен и сервер готов принимать запросы.

Вы увидите сообщения:

Точный внешний вид и дополнительные строки зависят от версии FastAPI и операционной системы. Нам важен порядок: сначала ожидание startup, затем его успешное завершение и только после этого готовность сервера.

Если конфигурация неправильная, сообщение Application startup complete не появится. Например при отсутствующем LLM_API_KEY объект Settings выбросит ValidationError, lifespan не дойдёт до yield, а сервер не начнёт принимать запросы.

В консольном main.py мы преобразовывали такую ошибку в короткий пользовательский вывод. А при запуске сервера нам правильнее завершиться сразу и оставить техническую информацию в журнале запуска. Ведь клиент не должен получить приложение, внутри которого отсутствует обязательная конфигурация.

Что происходит при остановке?

Вернитесь в терминал с сервером и остановите процесс. Чтобы остановить сервер, нажмитеCtrl+C

Uvicorn перестанет принимать новые запросы и запустит shutdown-часть lifespan. Выполнение продолжится после yield, войдёт в finally и вызовет container.close().

Сначала ApplicationContainer.close() завершит ToolExecutor. Его пул потоков больше не будет принимать задачи. Затем закроется LLMClient и принадлежащий ему HTTP-клиент. Даже если закрытие исполнителя завершится ошибкой, внутренний finally контейнера всё равно попытается закрыть LLM-клиент.

Сигнал остановки не означает, что Python мгновенно обрывает код посередине. Сервер выполняет управляемое завершение и даёт приложению освободить ресурсы. Позднее для долгих запросов и потоковых ответов появятся дополнительные правила отмены, но основная граница времени жизни уже создана.

Обратите внимание, команда fastapi dev следит за изменениями файлов. Если сохранить Python-файл, сервер разработки перезапустит рабочий процесс. Старый экземпляр приложения выполнит shutdown, а новый снова пройдёт startup.

Поэтому в терминале можно несколько раз увидеть сообщения о запуске и завершении. Это нормальное поведение режима автоматической перезагрузки. В каждый момент запросы обслуживает контейнер текущего процесса, а ресурсы предыдущего экземпляра закрываются.

Из этого следует важное правило, а именно что startup-код должен создавать ресурсы предсказуемо, а shutdown-код должен корректно освобождать именно их. Нельзя рассчитывать, что объект будет существовать вечно только потому, что сервер обычно работает долго.

На этом первая граница AI-бэкенда готова, а именно наш сервер запускается, собирает зависимости один раз, отвечает на технический запрос и корректно завершает работу. В следующем разделе добавим типизированные HTTP-модели, маршруты анализа обращения и чата, единый формат ошибок и идентификатор запроса для диагностики.

Шаг 5

Какую основную роль выполняет FastAPI в проекте AI Support Desk?

Правильный ответ
  • ✅ Предоставляет HTTP-интерфейс поверх уже созданных прикладных сервисов

Шаг 6

Что вернёт FastAPI-клиенту этот маршрут?

@router.get("/health")
async def health() -> dict[str, str]:
    return {"status": "ok"}
Правильный ответ
  • ✅ HTTP 200 и JSON-объект {"status": "ok"}

Шаг 7

Какие утверждения о lifespan верны?

Выберите все правильные варианты.

Правильный ответ
  • ✅ Код до yield выполняется при запуске приложения
  • ✅ Код после yield подходит для закрытия общих ресурсов
  • ✅ Созданный контейнер можно сохранить в application.state

Шаг 8

Первый маршрут GET /health

В заготовке уже создан объект app — это FastAPI-приложение, которое принимает HTTP-запросы. Вам нужно добавить в него адрес GET /health, по которому клиент может проверить, запущен ли сервер.

Задача

Добавьте в приложение app асинхронную функцию-эндпоинт health для обработки запросов GET /health. Она должна вернуть фиксированный JSON-ответ с признаком исправной работы сервера.

Требования

  • Не создавайте новый объект FastAPI и не переименовывайте переменную app.
  • Зарегистрируйте маршрут декоратором @app.get("/health").
  • Назовите функцию-обработчик health.
  • Объявите обработчик через async def.
  • Не добавляйте обработчику аргументы.
  • Верните обычный словарь ровно с одной парой: {"status": "ok"}.
  • Не возвращайте JSON-строку и не задавайте статус вручную: для успешного GET-запроса FastAPI использует статус 200.

Пример результата

После запуска приложения клиент отправляет запрос:

GET /health

Ожидаемый HTTP-ответ:

200 OK
Content-Type: application/json

{"status":"ok"}
Правильный ответ
from fastapi import FastAPI

app = FastAPI(title="AI Support Desk API")

@app.get("/health")
async def health():
    return {"status": "ok"}

Шаг 9

Системный маршрутизатор с GET /health

В заготовке уже создан объект router — это маршрутизатор, в котором хранятся связанные HTTP-маршруты. Вам нужно добавить в него адрес GET /health, по которому клиент сможет проверить состояние сервера.

Задача

Добавьте в router асинхронную функцию-эндпоинт health, которая обрабатывает запросы GET /health и возвращает фиксированный ответ о состоянии сервера.

Требования

  • Не создавайте новый маршрутизатор и не переименовывайте переменную router.
  • Сохраните заданный тег "system".
  • Зарегистрируйте маршрут декоратором @router.get("/health").
  • Назовите функцию-обработчик health и объявите её через async def.
  • Обработчик не должен принимать аргументы.
  • Верните ровно {"status": "ok"}.
  • Не создавайте объект FastAPI внутри этой задачи: проверяющее окружение само подключит router к тестовому приложению.

Пример результата

После подключения маршрутизатора клиент отправляет запрос:

GET /health

Ожидаемый HTTP-ответ:

200 OK
Content-Type: application/json

{"status":"ok"}

APIRouter уже импортирован, а router = APIRouter(tags=["system"]) находится в редакторе. Автопроверка подключит маршрутизатор к FastAPI и выполнит HTTP-запрос.

Правильный ответ
from fastapi import APIRouter

router = APIRouter(tags=["system"])

@router.get("/health")
async def health():
    return {"status": "ok"}

Шаг 10

Фабрика FastAPI-приложения

В функцию create_api передаётся готовый маршрутизатор router с HTTP-маршрутами. Вам нужно создать FastAPI-приложение, подключить к нему этот маршрутизатор и вернуть готовый объект.

Задача

Реализуйте фабрику create_api(router). Внутри неё создайте FastAPI-приложение с указанными метаданными, подключите переданный router и верните готовый объект приложения.

Требования

  • Не изменяйте имя функции и аргумент router.
  • При каждом вызове создавайте новый экземпляр FastAPI.
  • Передайте конструктору title="AI Support Desk API" и version="1.0.0".
  • Подключите переданный маршрутизатор единственным вызовом application.include_router(router).
  • Верните созданный объект application.
  • Не создавайте маршруты заново внутри фабрики и не изменяйте переданный маршрутизатор.
  • После подключения все маршруты переданного объекта должны быть доступны через приложение.
Правильный ответ
from fastapi import FastAPI

def create_api(router) -> FastAPI:
    application = FastAPI(
        title="AI Support Desk API",
        version="1.0.0",
    )
    application.include_router(router)
    return application

Шаг 11

Lifespan для общего контейнера

В заготовке уже объявлен обработчик жизненного цикла lifespan, который FastAPI запускает при старте и остановке приложения. Вам нужно создать общий контейнер перед началом обработки запросов, сохранить его в состоянии приложения и закрыть при завершении работы.

Задача

Дополните асинхронный контекстный менеджер lifespan(application). Создайте настройки и контейнер, сохраните контейнер в application.state.container, а затем гарантированно вызовите его метод close() при завершении приложения.

Требования

  • Не изменяйте имя функции, аргумент и декоратор @asynccontextmanager.
  • До yield создайте настройки единственным вызовом Settings().
  • Передайте настройки в единственный вызов build_application(settings).
  • Сохраните полученный объект в application.state.container.
  • Выполните yield ровно один раз после сохранения контейнера.
  • Поместите yield в конструкцию try ... finally.
  • В блоке finally вызовите container.close().
  • Не создавайте FastAPI-приложение внутри lifespan и не вызывайте функцию жизненного цикла вручную.
Правильный ответ
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager

from fastapi import FastAPI

from app.application import build_application
from app.config import Settings

@asynccontextmanager
async def lifespan(application: FastAPI) -> AsyncIterator[None]:
    settings = Settings()
    container = build_application(settings)
    application.state.container = container

    try:
        yield
    finally:
        container.close()

Шаг 12

Сборка FastAPI-приложения с lifespan

В функцию create_api передаются готовый маршрутизатор health_router и обработчик жизненного цикла lifespan_handler. Вам нужно создать FastAPI-приложение, подключить к нему оба объекта и вернуть результат.

Задача

Реализуйте фабрику create_api(health_router, lifespan_handler). Создайте FastAPI-приложение с переданным lifespan_handler, подключите health_router и верните полученное приложение.

Требования

  • Не изменяйте имя функции и два её аргумента.
  • При каждом вызове создавайте новый экземпляр FastAPI.
  • Передайте title="AI Support Desk API", version="1.0.0" и lifespan=lifespan_handler в конструктор.
  • Подключите health_router через application.include_router(health_router).
  • Верните созданное приложение.
  • Не вызывайте lifespan_handler самостоятельно, FastAPI должен запускать его при старте сервера.
Правильный ответ
from fastapi import FastAPI

def create_api(health_router, lifespan_handler) -> FastAPI:
    application = FastAPI(
        title="AI Support Desk API",
        version="1.0.0",
        lifespan=lifespan_handler,
    )
    application.include_router(health_router)
    return application