This commit is contained in:
Nikidze
2025-10-31 22:08:55 +03:00
commit e3f8caf59f
78 changed files with 9249 additions and 0 deletions

87
README.md Normal file
View File

@@ -0,0 +1,87 @@
# ИИ-помощник туриста
Веб-сервис, который создает **персональные пешеходные маршруты**, понимая **свободную речь пользователя**. Наш AI-гид анализирует ваши желания, текущее местоположение и время, чтобы построить уникальную прогулку, которая подходит именно вам. Сервис доступен [здесь: https://gorkycode.nikidze.ru/](https://gorkycode.nikidze.ru/).
---
## Понимание контекста
В отличие от конкурентов, которые просто ищут по тегам, наш сервис **понимает логику и последовательность** запроса. Мы не просто находим "музей" и "кофе", мы строим маршрут так, как этого хотел пользователь.
Как система обработает внешне похожие запросы:
| Запрос пользователя | Интерпретация AI и результат |
| :--- | :--- |
| *«Хочу сходить в музей и попить кофе»* | Найдет музей и кофейню рядом. Порядок **не важен**. |
| *«Хочу попить кофе, потом в музей»* | Построит маршрут **сначала в кофейню**, а уже **потом в музей**. |
| *«Хочу походить по музеям, потом зайти за кофе»* | Включит в маршрут **несколько музеев**, а кофейня будет **финальной точкой**. |
| *«Хочу в музей или попить кофе, потом на канатку»* | Выберет **одно из двух** (музей или кафе), а затем **обязательно добавит** канатную дорогу. |
Эта способность понимать нюансы делает наш сервис по-настоящему персональным и умным.
---
## Основные возможности
- **Пошаговый интерфейс** — Удобный опрос в несколько шагов, который не перегружает пользователя и позволяет легко вернуться для корректировки.
- **Диалоговый режим** — Если данных не хватает, система задаст уточняющие вопросы, пока не получит всю необходимую информацию.
- **Умная маршрутизация** — Алгоритм учитывает время на дорогу пешком и посещение локаций, отсекая нерелевантные точки.
- **Гибкий анализ запроса** — AI (Gemini 2.5 Flash) извлекает теги, время и адрес, даже если они указаны неточно (например, *«свободен до 6 вечера»* или адрес с опечаткой).
- **Микросервисная архитектура** — Проект разбит на независимые Docker-контейнеры, что обеспечивает надежность и масштабируемость.
---
## Как это работает: от запроса до маршрута
1. **Сбор данных (UI + AI)**
Пользователь в свободной форме отвечает на вопросы о своих предпочтениях, времени и местоположении. Данные отправляются в основной сервис `input_to_route`.
2. **Анализ и структурирование (AI: Gemini 2.5 Flash)**
С помощью модели `Gemini 2.5 Flash` через `OpenRouter API` текстовый запрос преобразуется в структурированные данные:
```json
{
"tags": [["Музей"], ["Кофейня"]],
"user_location": "улица Ошарская, дом 14",
"time": 180
}
```
На этом же этапе нормализуется адрес пользователя с помощью отдельного сервиса на базе `libpostal`.
3. **Поиск кандидатов (База данных)**
Сервис находит в базе данных все объекты, соответствующие извлеченным тегам.
4. **Построение маршрута (OSRM)**
- Координаты пользователя и найденных точек передаются в `navigation-engine` (кастомная сборка `OSRM` с картой Нижнего Новгорода).
- Алгоритм отсекает слишком удаленные точки.
- Строится матрица временных затрат на перемещение между оставшимися точками.
- На основе матрицы и логики тегов ("и", "или", "потом") генерируется список подходящих по времени маршрутов.
- Из списка выбирается один (на данный момент — случайный).
5. **Генерация описания (AI)**
На основе выбранных точек маршрута `Gemini 2.5 Flash` генерирует красивое и связное текстовое описание прогулки для пользователя.
---
## Архитектура и технологии
Проект построен на основе микросервисной архитектуры и развернут в Docker-контейнерах на платформе `dockhost`. Такой подход обеспечивает изоляцию компонентов и простоту масштабирования.
| Компонент | Технологии | Роль |
| :--- | :--- | :--- |
| **web-ui** | **Vue 3, JavaScript** | Пользовательский интерфейс: пошаговый опрос и отображение результата. |
| **input_to_route** | **FastAPI (Python)** | **Мозг системы.** Оркестрирует весь процесс: общается с UI, AI, базой данных и другими сервисами. |
| **engine-wrapper** | **FastAPI (Python)** | Упрощенная обертка над движком OSRM для удобства взаимодействия. |
| **navigation-engine** | **OSRM Backend (C++)** | Высокопроизводительный движок для построения пешеходных маршрутов. |
| **libpostal-service** | **libpostal, REST API** | Сервис для нормализации и парсинга географических адресов. Вынесен в отдельный контейнер из-за большого веса библиотеки. |
| **AI-провайдер** | **OpenRouter API (Gemini 2.5 Flash)** | Предоставляет доступ к LLM для анализа запросов и генерации текста. |
| **Платформа** | **Dockhost** | Облачная платформа для хостинга Docker-контейнеров. |
---
## Почему наш стек эффективен
1. **FastAPI** — Идеально подходит для создания асинхронных API, которые связывают разные сервисы. Быстрый, современный и легок в освоении.
2. **Vue 3** — Реактивный фреймворк, который позволяет создавать динамичные и отзывчивые пользовательские интерфейсы, как наш пошаговый опросник.
3. **OSRM** — Один из самых быстрых open-source движков для маршрутизации. Использование кастомной сборки только для Нижнего Новгорода делает его еще более производительным.
4. **Gemini 2.5 Flash через OpenRouter** — Экономически эффективное решение. Модель обеспечивает высокую скорость, отличное качество понимания русского языка и следование сложным инструкциям, что является ядром нашего проекта.
5. **Микросервисы на Docker** — Стандарт индустрии. Позволяет независимо разрабатывать, обновлять и масштабировать части приложения. `libpostal` в отдельном контейнере — классический пример правильного применения этого подхода.

View File

@@ -0,0 +1,3 @@
__pycache__
.venv
project.toml

1
engine-wrapper/.env Normal file
View File

@@ -0,0 +1 @@
OSRM_URL=https://8msn-80q0-el3y.gw-1a.dockhost.net

10
engine-wrapper/.gitignore vendored Normal file
View File

@@ -0,0 +1,10 @@
# Python-generated files
__pycache__/
*.py[oc]
build/
dist/
wheels/
*.egg-info
# Virtual environments
.venv

View File

@@ -0,0 +1 @@
3.13

22
engine-wrapper/Dockerfile Normal file
View File

@@ -0,0 +1,22 @@
# Используем официальный образ Python
FROM python:3.13-slim
# Устанавливаем рабочую директорию внутри контейнера
WORKDIR /app
# Копируем файлы зависимостей
COPY requirements.txt .
# Устанавливаем зависимости
RUN pip install --no-cache-dir -r requirements.txt
# Копируем остальные файлы приложения
COPY . .
# Открываем порт, на котором будет слушать FastAPI
EXPOSE 8000
# Запускаем приложение с помощью Uvicorn
# --host 0.0.0.0 делает приложение доступным извне контейнера
# --port 8000 указывает порт, на котором слушает Uvicorn
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

0
engine-wrapper/README.md Normal file
View File

126
engine-wrapper/main.py Normal file
View File

@@ -0,0 +1,126 @@
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import List, Optional
import httpx
import os
from dotenv import load_dotenv
# Загрузка переменных окружения из .env файла
load_dotenv()
app = FastAPI(title="OSRM API Wrapper")
# --- Модели для запросов и ответов ---
# Входная модель для обоих эндпоинтов: массив точек
class PointsRequest(BaseModel):
points: List[List[float]] # Формат: [[lat, lon], [lat, lon], ...]
# Модель ответа для /table
class TableResponse(BaseModel):
distances: List[List[float]]
durations: List[List[float]]
# Модель для одного шага в маршруте (для /route)
class RouteStep(BaseModel):
location: List[float] # [lon, lat]
distance: float
type: str
modifier: Optional[str] = ""
name: str
# Модель ответа для /route
class RouteResponse(BaseModel):
steps: List[RouteStep]
# --- Конфигурация ---
# Чтение URL OSRM из переменной окружения
OSRM_BASE_URL = os.getenv("OSRM_URL")
if not OSRM_BASE_URL:
raise RuntimeError("Переменная окружения OSRM_URL не установлена.")
# --- Эндпоинты ---
@app.post("/table", response_model=TableResponse)
async def table(request: PointsRequest):
"""
Принимает список координат и возвращает матрицы расстояний и времени в пути между ними.
"""
if len(request.points) < 2:
raise HTTPException(status_code=400, detail="Нужно минимум 2 точки")
# OSRM ожидает формат lon,lat
coords = ";".join(f"{lon},{lat}" for lat, lon in request.points)
# Эндпоинт для Table service
url = f"{OSRM_BASE_URL}/table/v1/foot/{coords}?annotations=duration,distance"
print(f"Запрос к OSRM: {url}")
async with httpx.AsyncClient() as client:
try:
resp = await client.get(url, timeout=10)
resp.raise_for_status()
except httpx.RequestError as exc:
raise HTTPException(status_code=500, detail=f"Ошибка запроса к OSRM: {exc.request.url!r} - {exc}")
except httpx.HTTPStatusError as exc:
raise HTTPException(status_code=500, detail=f"OSRM вернул ошибку {exc.response.status_code}: {exc.response.text}")
data = resp.json()
distances = data.get("distances")
durations = data.get("durations")
if distances is None or durations is None:
raise HTTPException(status_code=500, detail="OSRM вернул некорректный ответ")
return TableResponse(distances=distances, durations=durations)
@app.post("/route", response_model=RouteResponse)
async def route(request: PointsRequest):
"""
Принимает две точки (начальную и конечную) и возвращает пошаговый маршрут.
"""
if len(request.points) != 2:
raise HTTPException(status_code=400, detail="Для маршрута нужно ровно 2 точки")
coords = ";".join(f"{lon},{lat}" for lat, lon in request.points)
# Эндпоинт для Route service с параметром steps=true
url = f"{OSRM_BASE_URL}/route/v1/foot/{coords}?steps=true"
print(f"Запрос к OSRM: {url}")
async with httpx.AsyncClient() as client:
try:
resp = await client.get(url, timeout=10)
resp.raise_for_status()
except httpx.RequestError as exc:
raise HTTPException(status_code=500, detail=f"Ошибка запроса к OSRM: {exc.request.url!r} - {exc}")
except httpx.HTTPStatusError as exc:
raise HTTPException(status_code=500, detail=f"OSRM вернул ошибку {exc.response.status_code}: {exc.response.text}")
data = resp.json()
if "routes" not in data or len(data["routes"]) == 0:
raise HTTPException(status_code=500, detail="OSRM не вернул маршруты")
# Извлекаем шаги из ответа
raw_steps = data["routes"][0]["legs"][0]["steps"]
formatted_steps = []
for step in raw_steps:
maneuver = step["maneuver"]
formatted_steps.append(
RouteStep(
location=[maneuver["location"][1], maneuver["location"][0]],
distance=step["distance"],
type=maneuver["type"],
modifier=maneuver.get("modifier"),
name=step.get("name", "")
)
)
return RouteResponse(steps=formatted_steps)

View File

@@ -0,0 +1,13 @@
[project]
name = "engine-wrapper"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
"dotenv>=0.9.9",
"fastapi>=0.119.1",
"httpx>=0.28.1",
"pydantic>=2.12.3",
"uvicorn>=0.38.0",
]

View File

@@ -0,0 +1,5 @@
fastapi
uvicorn
httpx
pydantic
python-dotenv

275
engine-wrapper/uv.lock generated Normal file
View File

@@ -0,0 +1,275 @@
version = 1
revision = 2
requires-python = ">=3.13"
[[package]]
name = "annotated-types"
version = "0.7.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
]
[[package]]
name = "anyio"
version = "4.11.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "idna" },
{ name = "sniffio" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c6/78/7d432127c41b50bccba979505f272c16cbcadcc33645d5fa3a738110ae75/anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4", size = 219094, upload-time = "2025-09-23T09:19:12.58Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097, upload-time = "2025-09-23T09:19:10.601Z" },
]
[[package]]
name = "certifi"
version = "2025.10.5"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/4c/5b/b6ce21586237c77ce67d01dc5507039d444b630dd76611bbca2d8e5dcd91/certifi-2025.10.5.tar.gz", hash = "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43", size = 164519, upload-time = "2025-10-05T04:12:15.808Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de", size = 163286, upload-time = "2025-10-05T04:12:14.03Z" },
]
[[package]]
name = "click"
version = "8.3.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/46/61/de6cd827efad202d7057d93e0fed9294b96952e188f7384832791c7b2254/click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4", size = 276943, upload-time = "2025-09-18T17:32:23.696Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", size = 107295, upload-time = "2025-09-18T17:32:22.42Z" },
]
[[package]]
name = "colorama"
version = "0.4.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
]
[[package]]
name = "dotenv"
version = "0.9.9"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "python-dotenv" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/b2/b7/545d2c10c1fc15e48653c91efde329a790f2eecfbbf2bd16003b5db2bab0/dotenv-0.9.9-py2.py3-none-any.whl", hash = "sha256:29cf74a087b31dafdb5a446b6d7e11cbce8ed2741540e2339c69fbef92c94ce9", size = 1892, upload-time = "2025-02-19T22:15:01.647Z" },
]
[[package]]
name = "engine-wrapper"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "dotenv" },
{ name = "fastapi" },
{ name = "httpx" },
{ name = "pydantic" },
{ name = "uvicorn" },
]
[package.metadata]
requires-dist = [
{ name = "dotenv", specifier = ">=0.9.9" },
{ name = "fastapi", specifier = ">=0.119.1" },
{ name = "httpx", specifier = ">=0.28.1" },
{ name = "pydantic", specifier = ">=2.12.3" },
{ name = "uvicorn", specifier = ">=0.38.0" },
]
[[package]]
name = "fastapi"
version = "0.119.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pydantic" },
{ name = "starlette" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a6/f4/152127681182e6413e7a89684c434e19e7414ed7ac0c632999c3c6980640/fastapi-0.119.1.tar.gz", hash = "sha256:a5e3426edce3fe221af4e1992c6d79011b247e3b03cc57999d697fe76cbf8ae0", size = 338616, upload-time = "2025-10-20T11:30:27.734Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b1/26/e6d959b4ac959fdb3e9c4154656fc160794db6af8e64673d52759456bf07/fastapi-0.119.1-py3-none-any.whl", hash = "sha256:0b8c2a2cce853216e150e9bd4faaed88227f8eb37de21cb200771f491586a27f", size = 108123, upload-time = "2025-10-20T11:30:26.185Z" },
]
[[package]]
name = "h11"
version = "0.16.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
]
[[package]]
name = "httpcore"
version = "1.0.9"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "h11" },
]
sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
]
[[package]]
name = "httpx"
version = "0.28.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "certifi" },
{ name = "httpcore" },
{ name = "idna" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
]
[[package]]
name = "idna"
version = "3.11"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
]
[[package]]
name = "pydantic"
version = "2.12.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "annotated-types" },
{ name = "pydantic-core" },
{ name = "typing-extensions" },
{ name = "typing-inspection" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f3/1e/4f0a3233767010308f2fd6bd0814597e3f63f1dc98304a9112b8759df4ff/pydantic-2.12.3.tar.gz", hash = "sha256:1da1c82b0fc140bb0103bc1441ffe062154c8d38491189751ee00fd8ca65ce74", size = 819383, upload-time = "2025-10-17T15:04:21.222Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a1/6b/83661fa77dcefa195ad5f8cd9af3d1a7450fd57cc883ad04d65446ac2029/pydantic-2.12.3-py3-none-any.whl", hash = "sha256:6986454a854bc3bc6e5443e1369e06a3a456af9d339eda45510f517d9ea5c6bf", size = 462431, upload-time = "2025-10-17T15:04:19.346Z" },
]
[[package]]
name = "pydantic-core"
version = "2.41.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/df/18/d0944e8eaaa3efd0a91b0f1fc537d3be55ad35091b6a87638211ba691964/pydantic_core-2.41.4.tar.gz", hash = "sha256:70e47929a9d4a1905a67e4b687d5946026390568a8e952b92824118063cee4d5", size = 457557, upload-time = "2025-10-14T10:23:47.909Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/13/d0/c20adabd181a029a970738dfe23710b52a31f1258f591874fcdec7359845/pydantic_core-2.41.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:85e050ad9e5f6fe1004eec65c914332e52f429bc0ae12d6fa2092407a462c746", size = 2105688, upload-time = "2025-10-14T10:20:54.448Z" },
{ url = "https://files.pythonhosted.org/packages/00/b6/0ce5c03cec5ae94cca220dfecddc453c077d71363b98a4bbdb3c0b22c783/pydantic_core-2.41.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7393f1d64792763a48924ba31d1e44c2cfbc05e3b1c2c9abb4ceeadd912cced", size = 1910807, upload-time = "2025-10-14T10:20:56.115Z" },
{ url = "https://files.pythonhosted.org/packages/68/3e/800d3d02c8beb0b5c069c870cbb83799d085debf43499c897bb4b4aaff0d/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94dab0940b0d1fb28bcab847adf887c66a27a40291eedf0b473be58761c9799a", size = 1956669, upload-time = "2025-10-14T10:20:57.874Z" },
{ url = "https://files.pythonhosted.org/packages/60/a4/24271cc71a17f64589be49ab8bd0751f6a0a03046c690df60989f2f95c2c/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:de7c42f897e689ee6f9e93c4bec72b99ae3b32a2ade1c7e4798e690ff5246e02", size = 2051629, upload-time = "2025-10-14T10:21:00.006Z" },
{ url = "https://files.pythonhosted.org/packages/68/de/45af3ca2f175d91b96bfb62e1f2d2f1f9f3b14a734afe0bfeff079f78181/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:664b3199193262277b8b3cd1e754fb07f2c6023289c815a1e1e8fb415cb247b1", size = 2224049, upload-time = "2025-10-14T10:21:01.801Z" },
{ url = "https://files.pythonhosted.org/packages/af/8f/ae4e1ff84672bf869d0a77af24fd78387850e9497753c432875066b5d622/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d95b253b88f7d308b1c0b417c4624f44553ba4762816f94e6986819b9c273fb2", size = 2342409, upload-time = "2025-10-14T10:21:03.556Z" },
{ url = "https://files.pythonhosted.org/packages/18/62/273dd70b0026a085c7b74b000394e1ef95719ea579c76ea2f0cc8893736d/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1351f5bbdbbabc689727cb91649a00cb9ee7203e0a6e54e9f5ba9e22e384b84", size = 2069635, upload-time = "2025-10-14T10:21:05.385Z" },
{ url = "https://files.pythonhosted.org/packages/30/03/cf485fff699b4cdaea469bc481719d3e49f023241b4abb656f8d422189fc/pydantic_core-2.41.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1affa4798520b148d7182da0615d648e752de4ab1a9566b7471bc803d88a062d", size = 2194284, upload-time = "2025-10-14T10:21:07.122Z" },
{ url = "https://files.pythonhosted.org/packages/f9/7e/c8e713db32405dfd97211f2fc0a15d6bf8adb7640f3d18544c1f39526619/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7b74e18052fea4aa8dea2fb7dbc23d15439695da6cbe6cfc1b694af1115df09d", size = 2137566, upload-time = "2025-10-14T10:21:08.981Z" },
{ url = "https://files.pythonhosted.org/packages/04/f7/db71fd4cdccc8b75990f79ccafbbd66757e19f6d5ee724a6252414483fb4/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:285b643d75c0e30abda9dc1077395624f314a37e3c09ca402d4015ef5979f1a2", size = 2316809, upload-time = "2025-10-14T10:21:10.805Z" },
{ url = "https://files.pythonhosted.org/packages/76/63/a54973ddb945f1bca56742b48b144d85c9fc22f819ddeb9f861c249d5464/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:f52679ff4218d713b3b33f88c89ccbf3a5c2c12ba665fb80ccc4192b4608dbab", size = 2311119, upload-time = "2025-10-14T10:21:12.583Z" },
{ url = "https://files.pythonhosted.org/packages/f8/03/5d12891e93c19218af74843a27e32b94922195ded2386f7b55382f904d2f/pydantic_core-2.41.4-cp313-cp313-win32.whl", hash = "sha256:ecde6dedd6fff127c273c76821bb754d793be1024bc33314a120f83a3c69460c", size = 1981398, upload-time = "2025-10-14T10:21:14.584Z" },
{ url = "https://files.pythonhosted.org/packages/be/d8/fd0de71f39db91135b7a26996160de71c073d8635edfce8b3c3681be0d6d/pydantic_core-2.41.4-cp313-cp313-win_amd64.whl", hash = "sha256:d081a1f3800f05409ed868ebb2d74ac39dd0c1ff6c035b5162356d76030736d4", size = 2030735, upload-time = "2025-10-14T10:21:16.432Z" },
{ url = "https://files.pythonhosted.org/packages/72/86/c99921c1cf6650023c08bfab6fe2d7057a5142628ef7ccfa9921f2dda1d5/pydantic_core-2.41.4-cp313-cp313-win_arm64.whl", hash = "sha256:f8e49c9c364a7edcbe2a310f12733aad95b022495ef2a8d653f645e5d20c1564", size = 1973209, upload-time = "2025-10-14T10:21:18.213Z" },
{ url = "https://files.pythonhosted.org/packages/36/0d/b5706cacb70a8414396efdda3d72ae0542e050b591119e458e2490baf035/pydantic_core-2.41.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ed97fd56a561f5eb5706cebe94f1ad7c13b84d98312a05546f2ad036bafe87f4", size = 1877324, upload-time = "2025-10-14T10:21:20.363Z" },
{ url = "https://files.pythonhosted.org/packages/de/2d/cba1fa02cfdea72dfb3a9babb067c83b9dff0bbcb198368e000a6b756ea7/pydantic_core-2.41.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a870c307bf1ee91fc58a9a61338ff780d01bfae45922624816878dce784095d2", size = 1884515, upload-time = "2025-10-14T10:21:22.339Z" },
{ url = "https://files.pythonhosted.org/packages/07/ea/3df927c4384ed9b503c9cc2d076cf983b4f2adb0c754578dfb1245c51e46/pydantic_core-2.41.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d25e97bc1f5f8f7985bdc2335ef9e73843bb561eb1fa6831fdfc295c1c2061cf", size = 2042819, upload-time = "2025-10-14T10:21:26.683Z" },
{ url = "https://files.pythonhosted.org/packages/6a/ee/df8e871f07074250270a3b1b82aad4cd0026b588acd5d7d3eb2fcb1471a3/pydantic_core-2.41.4-cp313-cp313t-win_amd64.whl", hash = "sha256:d405d14bea042f166512add3091c1af40437c2e7f86988f3915fabd27b1e9cd2", size = 1995866, upload-time = "2025-10-14T10:21:28.951Z" },
{ url = "https://files.pythonhosted.org/packages/fc/de/b20f4ab954d6d399499c33ec4fafc46d9551e11dc1858fb7f5dca0748ceb/pydantic_core-2.41.4-cp313-cp313t-win_arm64.whl", hash = "sha256:19f3684868309db5263a11bace3c45d93f6f24afa2ffe75a647583df22a2ff89", size = 1970034, upload-time = "2025-10-14T10:21:30.869Z" },
{ url = "https://files.pythonhosted.org/packages/54/28/d3325da57d413b9819365546eb9a6e8b7cbd9373d9380efd5f74326143e6/pydantic_core-2.41.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:e9205d97ed08a82ebb9a307e92914bb30e18cdf6f6b12ca4bedadb1588a0bfe1", size = 2102022, upload-time = "2025-10-14T10:21:32.809Z" },
{ url = "https://files.pythonhosted.org/packages/9e/24/b58a1bc0d834bf1acc4361e61233ee217169a42efbdc15a60296e13ce438/pydantic_core-2.41.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:82df1f432b37d832709fbcc0e24394bba04a01b6ecf1ee87578145c19cde12ac", size = 1905495, upload-time = "2025-10-14T10:21:34.812Z" },
{ url = "https://files.pythonhosted.org/packages/fb/a4/71f759cc41b7043e8ecdaab81b985a9b6cad7cec077e0b92cff8b71ecf6b/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc3b4cc4539e055cfa39a3763c939f9d409eb40e85813257dcd761985a108554", size = 1956131, upload-time = "2025-10-14T10:21:36.924Z" },
{ url = "https://files.pythonhosted.org/packages/b0/64/1e79ac7aa51f1eec7c4cda8cbe456d5d09f05fdd68b32776d72168d54275/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b1eb1754fce47c63d2ff57fdb88c351a6c0150995890088b33767a10218eaa4e", size = 2052236, upload-time = "2025-10-14T10:21:38.927Z" },
{ url = "https://files.pythonhosted.org/packages/e9/e3/a3ffc363bd4287b80f1d43dc1c28ba64831f8dfc237d6fec8f2661138d48/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6ab5ab30ef325b443f379ddb575a34969c333004fca5a1daa0133a6ffaad616", size = 2223573, upload-time = "2025-10-14T10:21:41.574Z" },
{ url = "https://files.pythonhosted.org/packages/28/27/78814089b4d2e684a9088ede3790763c64693c3d1408ddc0a248bc789126/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:31a41030b1d9ca497634092b46481b937ff9397a86f9f51bd41c4767b6fc04af", size = 2342467, upload-time = "2025-10-14T10:21:44.018Z" },
{ url = "https://files.pythonhosted.org/packages/92/97/4de0e2a1159cb85ad737e03306717637842c88c7fd6d97973172fb183149/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a44ac1738591472c3d020f61c6df1e4015180d6262ebd39bf2aeb52571b60f12", size = 2063754, upload-time = "2025-10-14T10:21:46.466Z" },
{ url = "https://files.pythonhosted.org/packages/0f/50/8cb90ce4b9efcf7ae78130afeb99fd1c86125ccdf9906ef64b9d42f37c25/pydantic_core-2.41.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d72f2b5e6e82ab8f94ea7d0d42f83c487dc159c5240d8f83beae684472864e2d", size = 2196754, upload-time = "2025-10-14T10:21:48.486Z" },
{ url = "https://files.pythonhosted.org/packages/34/3b/ccdc77af9cd5082723574a1cc1bcae7a6acacc829d7c0a06201f7886a109/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c4d1e854aaf044487d31143f541f7aafe7b482ae72a022c664b2de2e466ed0ad", size = 2137115, upload-time = "2025-10-14T10:21:50.63Z" },
{ url = "https://files.pythonhosted.org/packages/ca/ba/e7c7a02651a8f7c52dc2cff2b64a30c313e3b57c7d93703cecea76c09b71/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b568af94267729d76e6ee5ececda4e283d07bbb28e8148bb17adad93d025d25a", size = 2317400, upload-time = "2025-10-14T10:21:52.959Z" },
{ url = "https://files.pythonhosted.org/packages/2c/ba/6c533a4ee8aec6b812c643c49bb3bd88d3f01e3cebe451bb85512d37f00f/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6d55fb8b1e8929b341cc313a81a26e0d48aa3b519c1dbaadec3a6a2b4fcad025", size = 2312070, upload-time = "2025-10-14T10:21:55.419Z" },
{ url = "https://files.pythonhosted.org/packages/22/ae/f10524fcc0ab8d7f96cf9a74c880243576fd3e72bd8ce4f81e43d22bcab7/pydantic_core-2.41.4-cp314-cp314-win32.whl", hash = "sha256:5b66584e549e2e32a1398df11da2e0a7eff45d5c2d9db9d5667c5e6ac764d77e", size = 1982277, upload-time = "2025-10-14T10:21:57.474Z" },
{ url = "https://files.pythonhosted.org/packages/b4/dc/e5aa27aea1ad4638f0c3fb41132f7eb583bd7420ee63204e2d4333a3bbf9/pydantic_core-2.41.4-cp314-cp314-win_amd64.whl", hash = "sha256:557a0aab88664cc552285316809cab897716a372afaf8efdbef756f8b890e894", size = 2024608, upload-time = "2025-10-14T10:21:59.557Z" },
{ url = "https://files.pythonhosted.org/packages/3e/61/51d89cc2612bd147198e120a13f150afbf0bcb4615cddb049ab10b81b79e/pydantic_core-2.41.4-cp314-cp314-win_arm64.whl", hash = "sha256:3f1ea6f48a045745d0d9f325989d8abd3f1eaf47dd00485912d1a3a63c623a8d", size = 1967614, upload-time = "2025-10-14T10:22:01.847Z" },
{ url = "https://files.pythonhosted.org/packages/0d/c2/472f2e31b95eff099961fa050c376ab7156a81da194f9edb9f710f68787b/pydantic_core-2.41.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6c1fe4c5404c448b13188dd8bd2ebc2bdd7e6727fa61ff481bcc2cca894018da", size = 1876904, upload-time = "2025-10-14T10:22:04.062Z" },
{ url = "https://files.pythonhosted.org/packages/4a/07/ea8eeb91173807ecdae4f4a5f4b150a520085b35454350fc219ba79e66a3/pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:523e7da4d43b113bf8e7b49fa4ec0c35bf4fe66b2230bfc5c13cc498f12c6c3e", size = 1882538, upload-time = "2025-10-14T10:22:06.39Z" },
{ url = "https://files.pythonhosted.org/packages/1e/29/b53a9ca6cd366bfc928823679c6a76c7a4c69f8201c0ba7903ad18ebae2f/pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5729225de81fb65b70fdb1907fcf08c75d498f4a6f15af005aabb1fdadc19dfa", size = 2041183, upload-time = "2025-10-14T10:22:08.812Z" },
{ url = "https://files.pythonhosted.org/packages/c7/3d/f8c1a371ceebcaf94d6dd2d77c6cf4b1c078e13a5837aee83f760b4f7cfd/pydantic_core-2.41.4-cp314-cp314t-win_amd64.whl", hash = "sha256:de2cfbb09e88f0f795fd90cf955858fc2c691df65b1f21f0aa00b99f3fbc661d", size = 1993542, upload-time = "2025-10-14T10:22:11.332Z" },
{ url = "https://files.pythonhosted.org/packages/8a/ac/9fc61b4f9d079482a290afe8d206b8f490e9fd32d4fc03ed4fc698214e01/pydantic_core-2.41.4-cp314-cp314t-win_arm64.whl", hash = "sha256:d34f950ae05a83e0ede899c595f312ca976023ea1db100cd5aa188f7005e3ab0", size = 1973897, upload-time = "2025-10-14T10:22:13.444Z" },
]
[[package]]
name = "python-dotenv"
version = "1.2.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" },
]
[[package]]
name = "sniffio"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
]
[[package]]
name = "starlette"
version = "0.48.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a7/a5/d6f429d43394057b67a6b5bbe6eae2f77a6bf7459d961fdb224bf206eee6/starlette-0.48.0.tar.gz", hash = "sha256:7e8cee469a8ab2352911528110ce9088fdc6a37d9876926e73da7ce4aa4c7a46", size = 2652949, upload-time = "2025-09-13T08:41:05.699Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/be/72/2db2f49247d0a18b4f1bb9a5a39a0162869acf235f3a96418363947b3d46/starlette-0.48.0-py3-none-any.whl", hash = "sha256:0764ca97b097582558ecb498132ed0c7d942f233f365b86ba37770e026510659", size = 73736, upload-time = "2025-09-13T08:41:03.869Z" },
]
[[package]]
name = "typing-extensions"
version = "4.15.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
]
[[package]]
name = "typing-inspection"
version = "0.4.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
]
[[package]]
name = "uvicorn"
version = "0.38.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "h11" },
]
sdist = { url = "https://files.pythonhosted.org/packages/cb/ce/f06b84e2697fef4688ca63bdb2fdf113ca0a3be33f94488f2cadb690b0cf/uvicorn-0.38.0.tar.gz", hash = "sha256:fd97093bdd120a2609fc0d3afe931d4d4ad688b6e75f0f929fde1bc36fe0e91d", size = 80605, upload-time = "2025-10-18T13:46:44.63Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ee/d9/d88e73ca598f4f6ff671fb5fde8a32925c2e08a637303a1d12883c7305fa/uvicorn-0.38.0-py3-none-any.whl", hash = "sha256:48c0afd214ceb59340075b4a052ea1ee91c16fbc2a9b1469cca0e54566977b02", size = 68109, upload-time = "2025-10-18T13:46:42.958Z" },
]

View File

@@ -0,0 +1,3 @@
__pycache__
.venv
.env

3
input-to-route/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
__pycache__
.venv
.env

View File

@@ -0,0 +1 @@
3.13

13
input-to-route/Dockerfile Normal file
View File

@@ -0,0 +1,13 @@
FROM python:3.13-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

0
input-to-route/README.md Normal file
View File

Binary file not shown.

View File

@@ -0,0 +1,94 @@
import json
def search_database(database_file, query):
"""
Поиск по базе данных с группировкой тегов по приоритетам.
Args:
database: список словарей с данными
query: список списков тегов, например [["Памятник"], ["Архитектура"]]
Returns:
список найденных записей с добавленными полями priority и измененным category_id
"""
with open(database_file, 'r', encoding='utf-8') as f:
database = json.load(f)
tag_mapping = {}
tag_counter = 1
# Создаем маппинг тегов
any_mode = False
for priority_group_idx, tag_group in enumerate(query, start=1):
if any_mode:
tag_counter+=1
if 'ANY' in tag_group:
tag_group.remove('ANY')
any_mode = True
else:
any_mode = False
if 'MULTI' in tag_group:
tag_group.remove('MULTI')
multi_mode = True
else:
multi_mode = False
for tag in tag_group:
if tag not in tag_mapping:
tag_mapping[tag] = {
'priority': priority_group_idx,
'tag_number': tag_counter,
'multi': multi_mode
}
if not any_mode:
tag_counter += 1
# Поиск и обработка записей
results = []
seen_ids = set()
for entry in database:
categories = entry.get('category_id')
categories =list(categories.split(', '))
found = False
for ctg in categories:
if ctg in tag_mapping:
found = True
category= ctg
break
if found:
entry_id = (entry.get('coordinate'), entry.get('title'))
if entry_id not in seen_ids:
seen_ids.add(entry_id)
result_entry = entry.copy()
result_entry['type'] = tag_mapping[category]['tag_number']
result_entry['priority'] = tag_mapping[category]['priority']
results.append(result_entry)
# Сортируем по приоритету
tag_priority = {v['tag_number']: (v['priority'],v['multi']) for v in tag_mapping.values()}
# Then verify no conflicts
if len(set((v['tag_number'], v['priority']) for v in tag_mapping.values())) != len(tag_priority):
raise ValueError("Conflicting priorities for same tag_number")
return results,tag_priority
# Пример использования
if __name__ == "__main__":
# Загрузка базы данных из файла
with open('output.json', 'r', encoding='utf-8') as f:
database = json.load(f)
# Запрос с группами тегов
query = [["Памятник", "Музей"], ["Архитектура"], ["Парк", "Сквер"]]
# Выполнение поиска
results = search_database(database, query)
print(results)
# Сохранение результатов
with open('search_results.json', 'w', encoding='utf-8') as f:
json.dump(results, f, ensure_ascii=False, indent=2)
print(f"Найдено записей: {len(results)}")

View File

@@ -0,0 +1,37 @@
from dotenv import load_dotenv
import os
import openai
class GeminiModel:
def __init__(self):
load_dotenv()
self.openrouter_api_key = os.getenv("OPENROUTER_API_KEY")
self.openrouter_api_url = os.getenv("OPENROUTER_API_URL", "https://openrouter.ai/api/v1")
def call(self, prompt):
if not self.openrouter_api_key:
raise ValueError("OPENROUTER_API_KEY is not set in environment variables.")
client = openai.OpenAI(
base_url=self.openrouter_api_url,
api_key=self.openrouter_api_key,
)
try:
response = client.chat.completions.create(
model="google/gemini-2.5-flash",
temperature=0.5,
messages=[
{"role": "user", "content": prompt}
]
)
data = response.to_dict()
return data['choices'][0]['message']['content']
except openai.APIError as e:
print(f"OpenRouter API call failed: {e}")
raise
if __name__ == "__main__":
model = GeminiModel()
print(model.call("Сколько будет 2 + 2?"))

386
input-to-route/geocoder.py Normal file
View File

@@ -0,0 +1,386 @@
import re
import sqlite3
from typing import Optional, Dict, Any, Tuple, List
import json
from rapidfuzz import process, fuzz, utils
import math
from libpostal_service import LibpostalService
lp_service = LibpostalService()
# ---------------------------
# Минимальная предобработка
# ---------------------------
_DASHES = {"": "-", "": "-", "": "-"}
def _preclean_text(s: str) -> str:
"""Минимальная очистка для устойчивости парсера: регистр, дефисы, пробелы, буква-цифра разделение."""
s = (s or "").strip().lower().replace("ё", "е")
for k, v in _DASHES.items():
s = s.replace(k, v)
s = re.sub(r"\s+", " ", s)
# Вставить пробел на границе буква<->цифра, чтобы "дом43" -> "дом 43"
s = re.sub(r"(?<=[A-Za-zА-Яа-я])(?=\d)|(?<=\d)(?=[A-Za-zА-Яа-я])", " ", s)
return s.strip()
def _parts_from_libpostal(addr_text: str) -> Dict[str, str]:
"""Собирает метка->значение из parse_address без сложных правил, работая с ответом от сервиса."""
parts: Dict[str, str] = {}
lp_response = lp_service.parse_address(addr_text)
for item in lp_response:
label = item.get('label')
comp = item.get('value')
if not comp or not label:
continue
comp = " ".join(comp.split())
if not comp:
continue
parts[label] = (parts[label] + " " + comp).strip() if label in parts else comp
return parts
def _prefer_cyrillic_min(expansions: List[str]) -> Optional[str]:
"""Выбрать «лучший» вариант из expand_address: предпочесть кириллицу и минимальную длину."""
if not expansions:
return None
cyr = [e for e in expansions if re.search(r"[А-Яа-я]", e)]
pool = cyr if cyr else expansions
pool = [" ".join(e.split()) for e in pool]
pool.sort(key=len)
return pool[0] if pool else None
def canonicalize_road(road: str) -> str:
"""Канонизация названия улицы через expand_address с минимальной эвристикой выбора."""
if not road:
return ""
exp = lp_service.expand_address(road)
return _prefer_cyrillic_min(exp) or road
def _clean_token(s: str) -> str:
return " ".join((s or "").strip().lower().replace("ё", "е").split())
def _unit_suffix_and_value(text: str) -> str:
"""
Преобразует текст unit/доп.компонента в компактный русскоязычный суффикс:
- 'корпус 1' / 'к 1' -> 'К1'
- 'строение 2' / 'стр 2' -> 'С2'
- 'лит а' -> 'ЛА'
- 'влад 5' -> 'ВЛ5'
Если тип не распознан, по умолчанию считаем корпус 'К' (без тяжёлых регексов).
"""
t = _clean_token(text)
# выделяем буквы/цифры (минимум регексов: только удалить пробелы)
val = t.replace(" ", "")
# эвристики по подстрокам
if "строен" in t or t.startswith("стр"):
# строение
val = val.replace("строение", "").replace("стр", "")
return f"С{val}".upper() if val else "С"
if "корп" in t or t == "к" or t.startswith("к"):
# корпус
val = val.replace("корпус", "").replace("корп", "").replace("к", "")
return f"К{val}".upper() if val else "К"
if t.startswith("лит"):
# литера
val = val.replace("литера", "").replace("лит", "")
return f"Л{val}".upper() if val else "Л"
if t.startswith("влад"):
# владение
val = val.replace("владение", "").replace("влад", "")
return f"ВЛ{val}".upper() if val else "ВЛ"
# вход/подъезд/лестница можно при желании кодировать как 'П', 'ЛС', но обычно это не часть addr:housenumber в OSM
# по умолчанию — трактуем как корпус
# оставим только цифро-буквенную часть после удаления пробелов
return f"К{val}".upper() if val else ""
def build_house_number_from_parts(parts: Dict[str, str], original_line: str) -> str:
"""
Формирует финальный housenumber из libpostal:
- base = house_number
- добавляет суффиксы из unit/entrance/staircase/level, конвертируя их в 'К..'/'С..'/'Л..'/'ВЛ..'
"""
base = (parts.get("house_number") or "").strip()
# защитимся от «дом43» и т.п. минимальной разделиловкой (сделано ранее в _preclean_text)
suffixes = []
# unit часто содержит корпус/строение
if parts.get("unit"):
suffixes.append(_unit_suffix_and_value(parts["unit"]))
# некоторые адреса могут класть строение/литеру в entrance/staircase
for key in ("entrance", "staircase"):
if parts.get(key):
sfx = _unit_suffix_and_value(parts[key])
if sfx:
suffixes.append(sfx)
# level — это этаж, обычно не часть housenumber в OSM; по умолчанию не добавляем
# если базовый номер пуст — попробуем через expand_address всей строки (без сложных правил)
if not base:
for e in lp_service.expand_address(original_line):
p2 = _parts_from_libpostal(e)
b2 = (p2.get("house_number") or "").strip()
if b2:
base = b2
if p2.get("unit") and not suffixes:
suffixes.append(_unit_suffix_and_value(p2["unit"]))
break
# склеиваем: пробелы убираем, верхний регистр для стабильности
hn = "".join(base.split()).upper() + "".join(suffixes)
return hn
def normalize_street_for_index(street_original: str) -> str:
"""Нормализация ключа улицы для индекса (регистронезависимая, с лексической очисткой)."""
# Используем обработчик RapidFuzz по умолчанию + канонизацию libpostal
street_clean = _preclean_text(street_original)
street_canon = canonicalize_road(street_clean)
return utils.default_process(street_canon) or street_canon
def normalize_house_for_index(house_original: str) -> str:
"""Простая нормализация номера для индекса: убрать пробелы, привести к верхнему регистру."""
return "".join((house_original or "").split()).upper()
# ---------------------------
# Поиск/валидация адреса
# ---------------------------
def parse_with_libpostal(addr: str) -> Dict[str, str]:
"""
Минимально-шумоустойчивая раскладка через libpostal.
"""
txt = _preclean_text(addr)
parts = _parts_from_libpostal(txt)
print("Parts:", parts)
road = parts.get("road") or parts.get("pedestrian") or parts.get("footway") or parts.get("residential") or ""
house = build_house_number_from_parts(parts, txt)
city = parts.get("city") or parts.get("city_district") or parts.get("suburb") or ""
postcode = parts.get("postcode", "")
# Канонизируем улицу для сравнения
road_canon = canonicalize_road(road)
return {
"road": road_canon or road or "",
"house_number": house or "",
"city": city or "",
"postcode": postcode or ""
}
def best_street_match(conn: sqlite3.Connection, street_query: str, cutoff: int = 86) -> Optional[Tuple[str, str]]:
"""
Ищет с учётом опечаток: exact по нормализованному ключу, иначе WRatio к ближайшему.
"""
s_key = normalize_street_for_index(street_query)
cur = conn.execute("SELECT street_original FROM street_names WHERE street_norm = ?", (s_key,))
row = cur.fetchone()
if row:
return (s_key, row[0])
cur = conn.execute("SELECT street_norm, street_original FROM street_names")
rows = cur.fetchall()
if not rows:
return None
choices = [r[0] for r in rows]
match = process.extractOne(s_key, choices, scorer=fuzz.WRatio, processor=None, score_cutoff=cutoff)
if match:
matched_norm = match[0]
for s_n, s_o in rows:
if s_n == matched_norm:
return (s_n, s_o)
return None
def _base_digits(s: str) -> str:
m = re.search(r"\d+", s or "")
return m.group(0) if m else ""
def best_house_match(conn, street_norm: str, house_query: str, cutoff: int = 82) -> Optional[Tuple[str, str]]:
# Простой канон запроса: убрать пробелы, верхний регистр
q_norm = ("".join((house_query or "").split())).upper()
q_base = _base_digits(q_norm)
# Вытаскиваем все номера по улице
cur = conn.execute(
"SELECT housenumber_norm, housenumber_original FROM address_index WHERE street_norm = ?",
(street_norm,)
)
rows = cur.fetchall()
if not rows:
return None
# 1) Точное совпадение целиком
for hn, ho in rows:
if hn == q_norm:
return (hn, ho)
# 2) Жёсткий фильтр по базовому числу (обязательное совпадение)
if q_base:
filtered: List[Tuple[str, str]] = [(hn, ho) for hn, ho in rows if _base_digits(hn) == q_base]
else:
filtered = rows
if not filtered:
return None
# 3) Нечёткий матч только среди отфильтрованных кандидатов
choices = [hn for hn, _ in filtered]
match = process.extractOne(q_norm, choices, scorer=fuzz.WRatio, processor=None, score_cutoff=cutoff)
if match:
best_hn = match[0]
for hn, ho in filtered:
if hn == best_hn:
return (hn, ho)
# 4) Fallback: если остался один кандидат с таким же базовым числом — принять его
if len(filtered) == 1:
return filtered[0]
return None
def _interp_point_along(coords: List[List[float]], t: float) -> Dict[str,float]:
# coords: [[lon,lat], ...], t∈[0,1] по длине полилинии
def dist(a,b):
dx=a[0]-b[0]; dy=a[1]-b[1]; return math.hypot(dx, dy)
seg_len=[]; total=0.0
for i in range(len(coords)-1):
d=dist(coords[i], coords[i+1]); seg_len.append(d); total += d
if total==0.0:
return {"lat": coords[0][1], "lon": coords[0][0]}
target = t*total; acc=0.0
for i,d in enumerate(seg_len):
if acc + d >= target:
ratio = 0.0 if d==0 else (target-acc)/d
lon = coords[i][0] + (coords[i+1][0]-coords[i][0])*ratio
lat = coords[i][1] + (coords[i+1][1]-coords[i][1])*ratio
return {"lat": float(lat), "lon": float(lon)}
acc += d
return {"lat": coords[-1][1], "lon": coords[-1][0]}
def _fits_type(n:int, itype:str)->bool:
itype=(itype or "").lower()
if itype=="odd": return n%2==1
if itype=="even": return n%2==0
return True # 'all' или неизвестный тип
def find_interpolation_coord(conn: sqlite3.Connection, street_norm: str, house_query: str) -> Optional[Dict[str,float]]:
m = re.search(r"\d+", house_query or "")
if not m:
return None
n = int(m.group(0))
cur = conn.execute(
"SELECT itype, start_num, end_num, coords_json FROM interpolation_ways WHERE street_norm=?",
(street_norm,)
)
best=None
for itype, s_i, e_i, coords_json in cur.fetchall():
if n < min(s_i,e_i) or n > max(s_i,e_i):
continue
if not _fits_type(n, itype):
continue
coords = json.loads(coords_json)
# позиция вдоль диапазона с плавающей точкой
span = (e_i - s_i) if (e_i - s_i)!=0 else 1
t = (n - s_i) / span
t = max(0.0, min(1.0, t))
return _interp_point_along(coords, t)
return best
def get_street_centroid_any(conn: sqlite3.Connection, street_norm: str) -> Optional[Dict[str, float]]:
# 1) по адресным точкам
cur = conn.execute(
"SELECT AVG(lat), AVG(lon) FROM address_index WHERE street_norm=? AND lat IS NOT NULL AND lon IS NOT NULL",
(street_norm,)
)
lat, lon = cur.fetchone() or (None,None)
if lat is not None and lon is not None:
return {"lat": float(lat), "lon": float(lon)}
# 2) по геометрии улиц (highway)
cur = conn.execute("SELECT lat, lon FROM street_center WHERE street_norm=?",(street_norm,))
row = cur.fetchone()
if row:
return {"lat": float(row[0]), "lon": float(row[1])}
return None
def validate_address(sqlite_path: str, address_str: str) -> Dict[str, Any]:
comps = parse_with_libpostal(address_str)
street_in = comps.get("road", "")
house_in = comps.get("house_number", "")
conn = sqlite3.connect(sqlite_path)
try:
# улица обязательна
if not street_in:
return {
"valid": False,
"corrected": None,
"components": comps,
"coordinates": None,
"reason": "Не удалось распознать улицу"
}
st = best_street_match(conn, street_in)
if not st:
return {
"valid": False,
"corrected": None,
"components": comps,
"coordinates": None,
"reason": "Улица не найдена в локальной базе"
}
street_norm, street_orig = st
# 1) дом не введён: вернуть координату улицы (centroid)
if not house_in:
cent = get_street_centroid_any(conn, street_norm)
return {
"valid": True if cent else False,
"corrected": street_orig,
"components": comps,
"coordinates": cent,
"reason": None if cent else "Для улицы нет координатной опоры"
}
# 2) дом введён: пробуем точное сопоставление
hn = best_house_match(conn, street_norm, house_in)
if hn:
house_norm, house_orig = hn
cur = conn.execute(
"SELECT city, postcode, lat, lon FROM address_index WHERE street_norm=? AND housenumber_norm=? LIMIT 1",
(street_norm, house_norm)
)
row = cur.fetchone()
city, postcode, lat, lon = (row or (None,None,None,None))
coords = {"lat": float(lat), "lon": float(lon)} if lat is not None and lon is not None else None
# если почему-то lat/lon пустые (редкий случай), дополнительно попробуем интерполяцию/центроид
if coords is None:
coords = find_interpolation_coord(conn, street_norm, house_norm) or get_street_centroid_any(conn, street_norm)
corrected = ", ".join(x for x in [f"{street_orig} {house_orig}", city, postcode] if x and str(x).strip())
return {
"valid": True,
"corrected": corrected,
"components": comps,
"coordinates": coords,
"reason": None
}
# 3) дом введён, но не найден: invalid, однако координаты улицы — обязательно
fallback = find_interpolation_coord(conn, street_norm, house_in) or get_street_centroid_any(conn, street_norm)
return {
"valid": False,
"corrected": None,
"components": comps,
"coordinates": fallback,
"reason": "Номер дома отсутствует на найденной улице"
}
finally:
conn.close()
if __name__ == "__main__":
res = validate_address("addresses.sqlite", "Дальняя 8")
print(res)
pass

View File

@@ -0,0 +1,38 @@
from dotenv import load_dotenv
import os
import requests
class LibpostalService:
def __init__(self):
load_dotenv()
self.libpostal_url = os.getenv("LIBPOSTAL_URL")
def parse_address(self, address_string):
if not self.libpostal_url:
raise ValueError("LIBPOSTAL_URL is not set in environment variables.")
url = f"{self.libpostal_url}/parse"
print(url)
params = {"address": address_string}
try:
response = requests.get(url, params=params)
response.raise_for_status() # Raise an exception for HTTP errors
return response.json()
except requests.exceptions.RequestException as e:
print(f"Error parsing address: {e}")
return None
def expand_address(self, address_string):
if not self.libpostal_url:
raise ValueError("LIBPOSTAL_URL is not set in environment variables.")
url = f"{self.libpostal_url}/expand"
params = {"address": address_string}
try:
response = requests.get(url, params=params)
response.raise_for_status() # Raise an exception for HTTP errors
return response.json()
except requests.exceptions.RequestException as e:
print(f"Error expanding address: {e}")
return None

143
input-to-route/main.py Normal file
View File

@@ -0,0 +1,143 @@
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from fastapi.middleware.cors import CORSMiddleware
from new_parser_test import parse_request
from geocoder import validate_address
from database import search_database
from route import build_route
from ouput_chat import RouteDescriber
app = FastAPI(title="AI API")
# ======== FASTAPI СХЕМЫ ==========
class ChatRequest(BaseModel):
user_input: str
conversation_history: list | None = None
class ChatResponse(BaseModel):
success: bool
need_more_info: bool
# result теперь может содержать как промежуточные данные парсера,
# так и финальный маршрут с описанием
result: dict
conversation_history: list
error: str | None = None
# =================================
@app.post("/chat", response_model=ChatResponse)
def chat_endpoint(request: ChatRequest):
try:
# 1. Вызываем парсер, чтобы извлечь информацию из диалога
parser_response = parse_request(request.user_input, request.conversation_history)
# 2. Проверяем условие: парсер отработал успешно и больше информации не нужно
if parser_response["success"] and not parser_response["need_more_info"]:
# --- Логика построения маршрута ---
parsed_data = parser_response["result"]
tags = parsed_data.get("tags")
print(tags)
user_address = parsed_data.get("user_location")
print(user_address)
user_time = parsed_data.get("time")
print(user_time)
conversation_history = parser_response["conversation_history"]
# Валидация адреса
val_output = validate_address('addresses.sqlite', user_address)
if not val_output or not val_output['valid']:
return ChatResponse(
success=False,
need_more_info=False,
result={},
conversation_history=conversation_history,
error=f"Не удалось распознать адрес: {user_address}. Пожалуйста, попробуйте указать его точнее."
)
user_position = [
val_output['coordinates']['lat'],
val_output['coordinates']['lon']
]
# Поиск точек в базе данных
found_points, mapping = search_database('output.json', tags)
if not found_points:
return ChatResponse(
success=False,
need_more_info=False,
result={},
conversation_history=conversation_history,
error="К сожалению, я не смог найти подходящих мест по вашему запросу."
)
print(mapping)
# Построение маршрута
n_nodes = len(mapping)
allow_extend = any(v[1] for v in mapping.values())
route_otp = None
if allow_extend:
while n_nodes <= 5 and (route_otp := build_route(found_points, mapping, user_position, user_time, n_nodes,strategy='random') or route_otp): n_nodes += 1
else:
route_otp= build_route(found_points, mapping, user_position, user_time, n_nodes,strategy='random')
if not route_otp:
return ChatResponse(
success=False,
need_more_info=False,
result={},
conversation_history=conversation_history,
error="Не удалось построить маршрут с указанными параметрами. Попробуйте изменить запрос."
)
route, places = route_otp
route.insert(0, user_position)
# Генерация описания маршрута
describer = RouteDescriber()
description = describer.generate_route_description(places, conversation_history)
# Финальный успешный ответ с маршрутом
return ChatResponse(
success=True,
need_more_info=False,
result={
"route": route,
"description": description,
"places": places
},
conversation_history=conversation_history,
error=None
)
else:
# 3. Если парсеру нужно больше информации или он завершился неуспешно,
# просто возвращаем его ответ пользователю.
# `ChatResponse` и словарь от `parse_request` имеют одинаковую структуру.
return ChatResponse(**parser_response)
except Exception as e:
# Отлавливаем любые другие непредвиденные ошибки
# В рабочей среде здесь лучше добавить логирование
print(f"An unexpected error occurred: {e}")
raise HTTPException(status_code=500, detail="Произошла внутренняя ошибка сервера.")
origins = [
"http://localhost",
"http://localhost:5173",
"https://gorkycode.nikidze.ru"
]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
max_age=3600
)

View File

@@ -0,0 +1,381 @@
import json
from typing import Dict, Any, List, Optional
from datetime import datetime, timedelta
from gemini_model import GeminiModel
# Базовый список доступных тегов
TAG_LIST = [
"Памятник", "Архитектура", "Мозаика", "Панно",
"Парк", "Водоем", "Озеро", "Ботанический сад",
"Архитектура", "Площадь", "Набережная", "Канатная дорога",
"Сквер", "Музей", "Собор", "Церковь",
"Храм", "Театр", "Выставка", "Мост", "Художественное пространство", "Вокзал", "Фонтан", "Лестница", "Кинотеатр",
"Дом Культуры", "Планетарий", "Галерея", "Ресторан", "Бар", "Бистро", "Кафе", "Кофейня", "Библиотека"
]
def build_extraction_prompt(conversation_history: List[Dict[str, str]], tag_list: List[str]) -> str:
"""
Строим промпт для ИИ-парсера с историей диалога
"""
example_user_1 = "Я хочу культурно провести время, у меня 2 часа, я сейчас на улице Доброй около дома 1."
example_json_1 = {
"tags": [["Музей", "Галерея", "Театр","Выставка","ANY", 'MULTI']],
"user_location": "улица Добрая, дом 1",
"time": 120
}
example_user_2 = "Я хочу сходить в музей пройти по парку и где-нибудь поесть, у меня 3.5 часа, я на Коминтерна, 115"
example_json_2 = {
"tags": [["Музей", "Парк"], ["Ресторан", "Бистро", "Кафе", "ANY"]],
"user_location": "улица Коминтерна, дом 115",
"time": 210
}
example_user_3 = "Я хочу культурно провести время потом попить кофе, я свободен до 6 вечера, я сейчас на б-р 60 лет Октября, 9"
example_json_3 = {
"tags": [['Музей', 'Галерея', 'Театр', 'Выставка', 'Художественное пространство', 'Планетарий', 'ANY', 'MULTI'], ["Кофейня"]],
"user_location": "бульвар 60 лет Октября, дом 9",
"time": 'until 18:00'
}
example_user_4 = "Я хочу обойти места в которых можно попить кофе и зайти в музей, у меня 4 часа, я на дальней 8"
example_json_4 = {
"tags": [['Кофейня', 'MULTI'],
["Музей"]],
"user_location": "улица Дальняя, дом 8",
"time": 240
}
instr = f"""
Ты — ИИ-парсер на русском языке.
Задача: из диалога с пользователем извлечь строго JSON c полями:
- tags: упорядоченный список релевантных тегов ТОЛЬКО из данного tag_list (не придумывай новых), теги должны быть сгруппированы по приоритету посещения
- user_location: строка с текущим местоположением пользователя (примерный адрес), должен содержать название улицы/площади/... Но не может быть ориентиром на объект без конкретного адреса или чем-то абстрактным: дома, у памятника Пушкину, у станции метро, на автобусной остановке... не подходят, но я около площади Революции подходит.
- time: целое число минут на прогулку (преобразуй выражения типа "2 часа" -> 120, "полчаса" -> 30 и т.п.). Пользователь может указать до скольки он свободен тогда используй фиксированный формат until <время в 24 формате HH:MM>. 'Я хочу погулять до 6 вечера' -> тогда в поле время 'until 18:00'.
Работа с тегами:
Группируй теги по ПРИОРИТЕТУ посещения:
- Если пользователь говорит "сначала музей, потом кафе" -> [['Музей'], ['Кафе']]
- Если "хочу в музей и кафе" (без явного порядка) -> [['Музей', 'Кафе']]
- Если неопределённые предпочтения "погулять по интересным местам", добавь ключевое слово 'ANY' к группе подходящих тегов.
- Если пользователь хочет посетить несколько мест одного типа, добавь ключевое слово 'MULTI' в соответсвующую группу. "Хочу походить по музеям, потом попить кофе." -> [['Музей','MULTI'], ['Кофейня']]
- Запрос пользователя может быть абстрактным "я хочу погулять по улице". Подбери релевантные теги на открытом воздухе (т.к. погулять можно интерпретировать как походить по улице не заходя куда-либо) например: [['Архитектура','Сквер','Площадь', 'ANY']]. Запрос может быть абстрактным, но не может быть полностью неопределенным. Примеры неопределенных ответов: "Хочу куда-нибудь сходить", "Я не знаю куда сходить", "Куда мне сходить?"
- Указывай все теги из tag_list которые явно подходят!!! Примеры могут быть неполными!
ИСПОЛЬЗУЙ ТОЛЬКО ТЕГИ ИЗ ДАННОГО СПИСКА!
Требования:
- Верни ТОЛЬКО валидный JSON без префиксов, комментариев и форматирования в кодовых блоках.
- Если в диалоге с пользователем нет данных для какого-либо поля, поставь null в этом поле.
- Не добавляй никаких дополнительных полей, только: tags, user_location, time.
- tags должны быть подмножеством из tag_list. Не включай нерелевантные теги.
tag_list = {json.dumps(tag_list, ensure_ascii=False, indent=0)}
Пример 1:
Пользователь: "{example_user_1}"
Ожидаемый JSON:
{json.dumps(example_json_1, ensure_ascii=False, indent=2)}
Пример 2:
Пользователь: "{example_user_2}"
Ожидаемый JSON:
{json.dumps(example_json_2, ensure_ascii=False, indent=2)}
Пример 3:
Пользователь: "{example_user_3}"
Ожидаемый JSON:
{json.dumps(example_json_3, ensure_ascii=False, indent=2)}
Пример 4:
Пользователь: "{example_user_4}"
Ожидаемый JSON:
{json.dumps(example_json_4, ensure_ascii=False, indent=2)}
История диалога:
"""
for msg in conversation_history:
role = msg["role"]
content = msg["content"]
if role == "user":
instr += f"\nПользователь: \"{content}\""
elif role == "assistant":
instr += f"\nАссистент: \"{content}\""
instr += "\n\nТеперь извлеки JSON из диалога выше."
return instr.strip()
def build_conversational_prompt(conversation_history: List[Dict[str, str]], missing: List[str],
tag_list: List[str]) -> str:
"""
Строим промпт для генерации уточняющего вопроса
"""
missing_desc = {
"tags": "какие места/категории интересны",
"user_location": "текущее местоположение (адрес)",
"time": "время на прогулку"
}
missing_fields_str = ", ".join([missing_desc[f] for f in missing])
instr = f"""
Ты — вежливый ИИ-ассистент, который помогает пользователю спланировать прогулку.
Твоя задача — задать краткие вопросы, чтобы уточнить недостающую информацию.
Недостающие данные: {missing_fields_str}
Правила:
- Задай ТОЛЬКО простык, вежливый и краткий вопрос.
- НЕ давай рекомендаций, комментариев или предложений.
- НЕ предлагай варианты, кроме случаев когда нужно выбрать категории (tags).
- Если нужно уточнить категории (tags), предложи категории ИЗ ЭТОГО СПИСКА: {json.dumps(tag_list, ensure_ascii=False)}
- Верни ТОЛЬКО текст вопроса без дополнительных пояснений.
История диалога:
"""
for msg in conversation_history:
role = msg["role"]
content = msg["content"]
if role == "user":
instr += f"\nПользователь: {content}"
elif role == "assistant":
instr += f"\nАссистент: {content}"
instr += "\n\nТеперь задай уточняющий вопрос:"
return instr.strip()
def call_gemini(prompt: str) -> str:
return GeminiModel().call(prompt)
def strip_code_fences(text: str) -> str:
s = text.strip()
# Убираем ограду `````` или ``````
if s.startswith("```"):
# срезаем первые ```
s = s[3:].lstrip()
# если указан язык (например, json), уберём первую строку
first_nl = s.find("\n")
if first_nl != -1:
lang = s[:first_nl].strip().lower()
# если это похоже на метку языка, отбрасываем её
if lang in ("json", "yaml", "yml", "xml", "markdown", "md", "txt"):
s = s[first_nl + 1:]
else:
# если метка не признана, всё равно продолжаем с текущим s
pass
# убираем завершающие ```
if s.rstrip().endswith("```"):
s = s.rstrip()[:-3]
return s.strip()
def extract_first_json(text: str):
# 1) снимаем ограду
s = strip_code_fences(text)
# 2) пробуем распарсить как есть
try:
return json.loads(s)
except Exception:
pass
# 3) ищем первый сбалансированный объект { ... } без рекурсивного regex
start = s.find("{")
while start != -1:
depth = 0
in_str = False
esc = False
for i in range(start, len(s)):
ch = s[i]
if in_str:
if esc:
esc = False
elif ch == "\\":
esc = True
elif ch == '"':
in_str = False
else:
if ch == '"':
in_str = True
elif ch == "{":
depth += 1
elif ch == "}":
depth -= 1
if depth == 0:
cand = s[start:i + 1]
try:
return json.loads(cand)
except Exception:
break
# ищем следующий '{', если текущий блок не распарсился
start = s.find("{", start + 1)
return None
def is_complete(result: Dict[str, Any]) -> bool:
if not isinstance(result, dict):
return False
# Проверяем наличие ключей
for key in ["tags", "user_location", "time"]:
if key not in result:
return False
# Проверяем содержимое
tags_ok = isinstance(result["tags"], list) and len(result["tags"]) > 0
loc_ok = isinstance(result["user_location"], str) and len(result["user_location"].strip()) > 0
# time может быть int или строка формата "until HH:MM"
time_val = result["time"]
if isinstance(time_val, int) and time_val > 0:
time_ok = True
elif isinstance(time_val, str) and time_val.startswith("until "):
time_ok = True
else:
time_ok = False
return tags_ok and loc_ok and time_ok
def missing_fields(result: Dict[str, Any]) -> List[str]:
missing = []
if not isinstance(result, dict):
return ["tags", "user_location", "time"]
if "tags" not in result or not isinstance(result["tags"], list) or len(result["tags"]) == 0:
missing.append("tags")
if "user_location" not in result or not isinstance(result["user_location"], str) or not result[
"user_location"].strip():
missing.append("user_location")
time_val = result.get("time")
if isinstance(time_val, int) and time_val > 0:
pass # OK
elif isinstance(time_val, str) and time_val.startswith("until "):
pass # OK
else:
missing.append("time")
return missing
def convert_time_to_minutes(time_val, current_time: datetime) -> int:
"""
Конвертирует значение времени в минуты.
Если time_val — это строка формата "until HH:MM", вычисляет разницу от текущего времени.
"""
if isinstance(time_val, int):
return time_val
if isinstance(time_val, str) and time_val.startswith("until "):
time_str = time_val.replace("until ", "").strip()
try:
target_time = datetime.strptime(time_str, "%H:%M")
# Устанавливаем дату как текущую
target_datetime = current_time.replace(
hour=target_time.hour,
minute=target_time.minute,
second=0,
microsecond=0
)
# Если целевое время уже прошло сегодня, предполагаем завтра
if target_datetime <= current_time:
target_datetime += timedelta(days=1)
delta = target_datetime - current_time
minutes = int(delta.total_seconds() / 60)
return minutes
except ValueError:
return None
return None
def extract_with_ai(conversation_history: List[Dict[str, str]]) -> Dict[str, Any]:
prompt = build_extraction_prompt(conversation_history, TAG_LIST)
model_text = call_gemini(prompt)
parsed = extract_first_json(model_text) or {}
return parsed
def ask_ai_for_clarification(conversation_history: List[Dict[str, str]], missing: List[str]) -> str:
"""
Генерируем уточняющий вопрос через ИИ
"""
prompt = build_conversational_prompt(conversation_history, missing, TAG_LIST)
question = call_gemini(prompt)
return question.strip()
def parse_request(user_input=None, conversation_history=None):
if conversation_history is None:
conversation_history = []
if len(conversation_history) > 8:
return {
"success": False,
"need_more_info": False,
"result": {},
"conversation_history": conversation_history,
"error": "Слишком длинная история диалога. Начните заново."
}
if user_input:
conversation_history.append({"role": "user", "content": user_input})
current_time = datetime.now()
result = extract_with_ai(conversation_history)
max_tries = 3
tries = 0
need_more_info = False
while not is_complete(result) and tries < max_tries:
missing = missing_fields(result)
if not missing:
break
ai_question = ask_ai_for_clarification(conversation_history, missing)
conversation_history.append({"role": "assistant", "content": ai_question})
need_more_info = True
break # API не ждёт ввода, просто возвращает вопрос пользователю
if "time" in result and isinstance(result["time"], str):
minutes = convert_time_to_minutes(result["time"], current_time)
if minutes is not None:
result["time"] = minutes
out = {
"tags": result.get("tags"),
"user_location": result.get("user_location"),
"time": result.get("time"),
}
success = is_complete(result)
return {
"success": success,
"need_more_info": need_more_info,
"result": out,
"conversation_history": conversation_history,
}
if __name__ == "__main__":
print("=== Консольная версия ===")
print("Введите ваш запрос (например: 'Хочу культурно провести время, потом попить кофе, я сейчас на улице Дальней около дома 8. У меня есть около двух часов')")
user_input = input("> ").strip()
response = parse_request(user_input=user_input)
print(json.dumps(response, ensure_ascii=False, indent=2))

View File

@@ -0,0 +1,164 @@
from typing import List, Dict, Optional
from gemini_model import GeminiModel
class RouteDescriber:
"""
Класс для работы с описанием маршрутов и ответами на вопросы пользователей через GeminiModel.
"""
def __init__(self):
self.model = GeminiModel()
self.conversation_history = []
self.route_points = []
self.route_description = ""
self.parser_chat_context = ""
def _call_model(self, prompt: str, system_instruction: Optional[str] = None) -> str:
"""
Вызывает GeminiModel с составленным промптом и системной инструкцией.
"""
full_prompt = prompt
if system_instruction:
full_prompt = f"Системная инструкция:\n{system_instruction}\n\n{prompt}"
return self.model.call(full_prompt)
def generate_route_description(self, route_points: List[Dict],
parser_chat_history: Optional[List[Dict]] = None) -> str:
"""
Генерирует описание маршрута на основе точек.
"""
self.route_points = route_points
self.conversation_history = []
# Формируем контекст парсера
if parser_chat_history:
print(parser_chat_history)
parser_messages = [f"{msg['role'].upper()}: {msg['content']}" for msg in parser_chat_history]
self.parser_chat_context = "\n".join(parser_messages)
else:
self.parser_chat_context = ""
# Формируем промпт с точками маршрута
points_info = []
for i, point in enumerate(route_points, 1):
name = point.get('title', f'Точка {i}')
description = point.get('description', '')
points_info.append(f"{i}. {name}\nОписание: {description}")
prompt = f"""Создай краткое описание маршрута из {len(route_points)} точек.
Для каждой точки напиши 1-2 предложения, которые заинтересуют посетителя.
Используй только информацию из описаний точек.
Точки маршрута:
{chr(10).join(points_info)}
"""
if self.parser_chat_context:
prompt = f"История запроса пользователя:\n{self.parser_chat_context}\n\n{prompt}"
system_instruction = """Ты — помощник для описания туристических маршрутов.
Твоя задача — создавать краткие, привлекательные описания точек маршрута (1-2 предложения на точку).
Используй только информацию, представленную в описаниях точек.
Пиши на русском языке."""
description = self._call_model(prompt, system_instruction)
self.route_description = description
return description
def answer_question(self, question: str) -> str:
"""
Отвечает на вопрос пользователя о точках маршрута.
"""
# Формируем контекст с информацией о точках
points_context = [f"{i+1}. {p['name']}: {p.get('description', 'Нет описания')}"
for i, p in enumerate(self.route_points)]
context = "\n\n".join(points_context)
# Дополнительный контекст парсера и описания маршрута
parser_info = f"\n\nИстория запроса пользователя:\n{self.parser_chat_context}" if self.parser_chat_context else ""
route_desc_info = f"\n\nОписание маршрута:\n{self.route_description}" if self.route_description else ""
system_instruction = f"""Ты — помощник по туристическому маршруту.
{parser_info}{route_desc_info}
Доступная информация о точках маршрута:
{context}
СТРОГИЕ ПРАВИЛА:
1. Отвечай ТОЛЬКО на вопросы о точках маршрута
2. Используй ТОЛЬКО информацию из описаний точек выше
3. Если в описаниях нет информации для ответа на вопрос, вежливо уходи от ответа фразами вида "Вы узнаете ответ, когда посетите это место", адаптируй под контекст.
4. НЕ давай рекомендаций и советов
5. НЕ отвечай на вопросы, не связанные с маршрутом. В таких случаях отвечай: "Я могу ответить только на вопросы о точках вашего маршрута"
6. НЕ придумывай информацию, которой нет в описаниях
7. Отвечай кратко и по существу на русском языке
8. Ты видишь всю историю разговора, поэтому можешь отвечать на уточняющие вопросы"""
answer = self._call_model(question, system_instruction)
return answer
def reset_conversation(self):
"""Очищает историю разговора и маршрута."""
self.conversation_history = []
self.route_points = []
self.route_description = ""
self.parser_chat_context = ""
# Пример использования
if __name__ == "__main__":
# Инициализация (замените на ваш API ключ)
API_KEY = "YOUR_GEMINI_API_KEY"
describer = RouteDescriber()
# Пример истории чата с парсером
parser_chat = [
{"role": "user", "text": "Хочу погулять по центру Москвы, интересует история и искусство"},
{"role": "model", "text": "Понял, вы хотите прогулку по историческому центру с посещением музеев. Сколько времени у вас есть?"},
{"role": "user", "text": "Часа 3-4"},
{"role": "model", "text": "Отлично, я составил маршрут из 3 точек по центру Москвы с историческими местами и музеем искусства."}
]
# Пример данных маршрута
route_points = [
{
"name": "Красная площадь",
"description": "Главная площадь Москвы, окружённая историческими зданиями. Здесь находятся Кремль, собор Василия Блаженного и Мавзолей Ленина. Площадь была основана в конце XV века."
},
{
"name": "Парк Горького",
"description": "Один из самых известных парков Москвы, расположенный на берегу Москвы-реки. Здесь есть зоны для отдыха, спортивные площадки, прокат велосипедов. Парк был открыт в 1928 году."
},
{
"name": "Третьяковская галерея",
"description": "Художественный музей с крупнейшей коллекцией русского искусства. В экспозиции представлены работы от древнерусских икон до картин XX века. Основана купцом Павлом Третьяковым."
}
]
# Генерация описания маршрута с историей парсера
print("=== ОПИСАНИЕ МАРШРУТА ===")
description = describer.generate_route_description(route_points, parser_chat)
print(description)
print()
# Примеры вопросов (теперь с контекстом истории)
print("=== ДИАЛОГ С ПОЛЬЗОВАТЕЛЕМ ===")
questions = [
"Когда был основан Парк Горького?",
"А что там можно делать?", # Уточняющий вопрос о предыдущей теме
"Какие картины есть в Третьяковской галерее?",
"Сколько стоит билет?", # Без контекста, но система поймет что это про музей
"Расскажи про Кремль",
"Что посоветуешь взять с собой?", # Попытка получить рекомендацию
"Как погода сегодня?" # Нерелевантный вопрос
]
for q in questions:
print(f"\n👤 Пользователь: {q}")
answer = describer.answer_question(q)
print(f"🤖 Бот: {answer}")

2576
input-to-route/output.json Normal file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,7 @@
import pandas as pd
# Читаем конкретный лист из Excel
df = pd.read_excel('cultural_objects_mnn_final.xlsx', sheet_name='list1')
# Сохраняем в JSON с красивым форматированием
df.to_json('output.json', orient='records', force_ascii=False, indent=4)

View File

@@ -0,0 +1,17 @@
[project]
name = "input-to-route"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
"dotenv>=0.9.9",
"fastapi>=0.120.2",
"openai>=2.6.1",
"openpyxl>=3.1.5",
"pandas>=2.3.3",
"pydantic>=2.12.3",
"rapidfuzz>=3.14.1",
"requests>=2.32.5",
"uvicorn>=0.38.0",
]

View File

@@ -0,0 +1,286 @@
# This file was autogenerated by uv via the following command:
# uv export --format requirements.txt
annotated-doc==0.0.3 \
--hash=sha256:348ec6664a76f1fd3be81f43dffbee4c7e8ce931ba71ec67cc7f4ade7fbbb580 \
--hash=sha256:e18370014c70187422c33e945053ff4c286f453a984eba84d0dbfa0c935adeda
# via fastapi
annotated-types==0.7.0 \
--hash=sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53 \
--hash=sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89
# via pydantic
anyio==4.11.0 \
--hash=sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc \
--hash=sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4
# via
# httpx
# openai
# starlette
certifi==2025.10.5 \
--hash=sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de \
--hash=sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43
# via
# httpcore
# httpx
# requests
charset-normalizer==3.4.4 \
--hash=sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152 \
--hash=sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72 \
--hash=sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e \
--hash=sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c \
--hash=sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2 \
--hash=sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44 \
--hash=sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede \
--hash=sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed \
--hash=sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133 \
--hash=sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e \
--hash=sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14 \
--hash=sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828 \
--hash=sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f \
--hash=sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328 \
--hash=sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090 \
--hash=sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c \
--hash=sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb \
--hash=sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a \
--hash=sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec \
--hash=sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc \
--hash=sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac \
--hash=sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894 \
--hash=sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14 \
--hash=sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1 \
--hash=sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3 \
--hash=sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e \
--hash=sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6 \
--hash=sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191 \
--hash=sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd \
--hash=sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2 \
--hash=sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794 \
--hash=sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838 \
--hash=sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490 \
--hash=sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9
# via requests
click==8.3.0 \
--hash=sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc \
--hash=sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4
# via uvicorn
colorama==0.4.6 ; sys_platform == 'win32' \
--hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \
--hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6
# via
# click
# tqdm
distro==1.9.0 \
--hash=sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed \
--hash=sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2
# via openai
dotenv==0.9.9 \
--hash=sha256:29cf74a087b31dafdb5a446b6d7e11cbce8ed2741540e2339c69fbef92c94ce9
# via input-to-route
fastapi==0.120.2 \
--hash=sha256:4c5ab43e2a90335bbd8326d1b659eac0f3dbcc015e2af573c4f5de406232c4ac \
--hash=sha256:bedcf2c14240e43d56cb9a339b32bcf15104fe6b5897c0222603cb7ec416c8eb
# via input-to-route
h11==0.16.0 \
--hash=sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1 \
--hash=sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86
# via
# httpcore
# uvicorn
httpcore==1.0.9 \
--hash=sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55 \
--hash=sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8
# via httpx
httpx==0.28.1 \
--hash=sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc \
--hash=sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad
# via openai
idna==3.11 \
--hash=sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea \
--hash=sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902
# via
# anyio
# httpx
# requests
jiter==0.11.1 \
--hash=sha256:090f4c9d4a825e0fcbd0a2647c9a88a0f366b75654d982d95a9590745ff0c48d \
--hash=sha256:09e2e386ebf298547ca3a3704b729471f7ec666c2906c5c26c1a915ea24741ec \
--hash=sha256:0a68d679c0e47649a61df591660507608adc2652442de7ec8276538ac46abe08 \
--hash=sha256:0b3de72e925388453a5171be83379549300db01284f04d2a6f244d1d8de36f94 \
--hash=sha256:0fa1f70da7a8a9713ff8e5f75ec3f90c0c870be6d526aa95e7c906f6a1c8c676 \
--hash=sha256:28e4fdf2d7ebfc935523e50d1efa3970043cfaa161674fe66f9642409d001dfe \
--hash=sha256:2aa9b1958f9c30d3d1a558b75f0626733c60eb9b7774a86b34d88060be1e67fe \
--hash=sha256:2cc5a3965285ddc33e0cab933e96b640bc9ba5940cea27ebbbf6695e72d6511c \
--hash=sha256:30405f726e4c2ed487b176c09f8b877a957f535d60c1bf194abb8dadedb5836f \
--hash=sha256:3217f61728b0baadd2551844870f65219ac4a1285d5e1a4abddff3d51fdabe96 \
--hash=sha256:330e8e6a11ad4980cd66a0f4a3e0e2e0f646c911ce047014f984841924729789 \
--hash=sha256:4573b78777ccfac954859a6eff45cbd9d281d80c8af049d0f1a3d9fc323d5c3a \
--hash=sha256:53a54bf8e873820ab186b2dca9f6c3303f00d65ae5e7b7d6bda1b95aa472d646 \
--hash=sha256:569ee559e5046a42feb6828c55307cf20fe43308e3ae0d8e9e4f8d8634d99944 \
--hash=sha256:5d761f863f912a44748a21b5c4979c04252588ded8d1d2760976d2e42cd8d991 \
--hash=sha256:5db4c2486a023820b701a17aec9c5a6173c5ba4393f26662f032f2de9c848b0f \
--hash=sha256:635ecd45c04e4c340d2187bcb1cea204c7cc9d32c1364d251564bf42e0e39c2d \
--hash=sha256:69dd514bf0fa31c62147d6002e5ca2b3e7ef5894f5ac6f0a19752385f4e89437 \
--hash=sha256:6b572b3636a784c2768b2342f36a23078c8d3aa6d8a30745398b1bab58a6f1a8 \
--hash=sha256:7042c51e7fbeca65631eb0c332f90c0c082eab04334e7ccc28a8588e8e2804d9 \
--hash=sha256:71b6a920a5550f057d49d0e8bcc60945a8da998019e83f01adf110e226267663 \
--hash=sha256:7593ac6f40831d7961cb67633c39b9fef6689a211d7919e958f45710504f52d3 \
--hash=sha256:7e29aca023627b0e0c2392d4248f6414d566ff3974fa08ff2ac8dbb96dfee92a \
--hash=sha256:849dcfc76481c0ea0099391235b7ca97d7279e0fa4c86005457ac7c88e8b76dc \
--hash=sha256:87202ec6ff9626ff5f9351507def98fcf0df60e9a146308e8ab221432228f4ea \
--hash=sha256:902640c3103625317291cb73773413b4d71847cdf9383ba65528745ff89f1d14 \
--hash=sha256:a1b0da75dbf4b6ec0b3c9e604d1ee8beaf15bc046fff7180f7d89e3cdbd3bb51 \
--hash=sha256:a5dd268f6531a182c89d0dd9a3f8848e86e92dfff4201b77a18e6b98aa59798c \
--hash=sha256:a83097ce379e202dcc3fe3fc71a16d523d1ee9192c8e4e854158f96b3efe3f2f \
--hash=sha256:aa22c223a3041dacb2fcd37c70dfd648b44662b4a48e242592f95bda5ab09d58 \
--hash=sha256:ad93e3d67a981f96596d65d2298fe8d1aa649deb5374a2fb6a434410ee11915e \
--hash=sha256:b1364cc90c03a8196f35f396f84029f12abe925415049204446db86598c8b72c \
--hash=sha256:b2ce0d6156a1d3ad41da3eec63b17e03e296b78b0e0da660876fccfada86d2f7 \
--hash=sha256:bb31ac0b339efa24c0ca606febd8b77ef11c58d09af1b5f2be4c99e907b11111 \
--hash=sha256:bbf3d8cedf9e9d825233e0dcac28ff15c47b7c5512fdfe2e25fd5bbb6e6b0cee \
--hash=sha256:cc19dd65a2bd3d9c044c5b4ebf657ca1e6003a97c0fc10f555aa4f7fb9821c00 \
--hash=sha256:d58faaa936743cd1464540562f60b7ce4fd927e695e8bc31b3da5b914baa9abd \
--hash=sha256:d892b184da4d94d94ddb4031296931c74ec8b325513a541ebfd6dfb9ae89904b \
--hash=sha256:e42d1ca16590b768c5e7d723055acd2633908baacb3628dd430842e2e035aa90 \
--hash=sha256:f153e31d8bca11363751e875c0a70b3d25160ecbaee7b51e457f14498fb39d8b \
--hash=sha256:f4db07d127b54c4a2d43b4cf05ff0193e4f73e0dd90c74037e16df0b29f666e1 \
--hash=sha256:f69955fa1d92e81987f092b233f0be49d4c937da107b7f7dcf56306f1d3fcce9 \
--hash=sha256:f773f84080b667c69c4ea0403fc67bb08b07e2b7ce1ef335dea5868451e60fed \
--hash=sha256:fe4a431c291157e11cee7c34627990ea75e8d153894365a3bc84b7a959d23ca8
# via openai
openai==2.6.1 \
--hash=sha256:27ae704d190615fca0c0fc2b796a38f8b5879645a3a52c9c453b23f97141bb49 \
--hash=sha256:904e4b5254a8416746a2f05649594fa41b19d799843cd134dac86167e094edef
# via input-to-route
pydantic==2.12.3 \
--hash=sha256:1da1c82b0fc140bb0103bc1441ffe062154c8d38491189751ee00fd8ca65ce74 \
--hash=sha256:6986454a854bc3bc6e5443e1369e06a3a456af9d339eda45510f517d9ea5c6bf
# via
# fastapi
# input-to-route
# openai
pydantic-core==2.41.4 \
--hash=sha256:19f3684868309db5263a11bace3c45d93f6f24afa2ffe75a647583df22a2ff89 \
--hash=sha256:1affa4798520b148d7182da0615d648e752de4ab1a9566b7471bc803d88a062d \
--hash=sha256:285b643d75c0e30abda9dc1077395624f314a37e3c09ca402d4015ef5979f1a2 \
--hash=sha256:31a41030b1d9ca497634092b46481b937ff9397a86f9f51bd41c4767b6fc04af \
--hash=sha256:3f1ea6f48a045745d0d9f325989d8abd3f1eaf47dd00485912d1a3a63c623a8d \
--hash=sha256:523e7da4d43b113bf8e7b49fa4ec0c35bf4fe66b2230bfc5c13cc498f12c6c3e \
--hash=sha256:557a0aab88664cc552285316809cab897716a372afaf8efdbef756f8b890e894 \
--hash=sha256:5729225de81fb65b70fdb1907fcf08c75d498f4a6f15af005aabb1fdadc19dfa \
--hash=sha256:5b66584e549e2e32a1398df11da2e0a7eff45d5c2d9db9d5667c5e6ac764d77e \
--hash=sha256:664b3199193262277b8b3cd1e754fb07f2c6023289c815a1e1e8fb415cb247b1 \
--hash=sha256:6c1fe4c5404c448b13188dd8bd2ebc2bdd7e6727fa61ff481bcc2cca894018da \
--hash=sha256:6d55fb8b1e8929b341cc313a81a26e0d48aa3b519c1dbaadec3a6a2b4fcad025 \
--hash=sha256:70e47929a9d4a1905a67e4b687d5946026390568a8e952b92824118063cee4d5 \
--hash=sha256:7b74e18052fea4aa8dea2fb7dbc23d15439695da6cbe6cfc1b694af1115df09d \
--hash=sha256:82df1f432b37d832709fbcc0e24394bba04a01b6ecf1ee87578145c19cde12ac \
--hash=sha256:85e050ad9e5f6fe1004eec65c914332e52f429bc0ae12d6fa2092407a462c746 \
--hash=sha256:94dab0940b0d1fb28bcab847adf887c66a27a40291eedf0b473be58761c9799a \
--hash=sha256:a1351f5bbdbbabc689727cb91649a00cb9ee7203e0a6e54e9f5ba9e22e384b84 \
--hash=sha256:a44ac1738591472c3d020f61c6df1e4015180d6262ebd39bf2aeb52571b60f12 \
--hash=sha256:a870c307bf1ee91fc58a9a61338ff780d01bfae45922624816878dce784095d2 \
--hash=sha256:b1eb1754fce47c63d2ff57fdb88c351a6c0150995890088b33767a10218eaa4e \
--hash=sha256:b568af94267729d76e6ee5ececda4e283d07bbb28e8148bb17adad93d025d25a \
--hash=sha256:c4d1e854aaf044487d31143f541f7aafe7b482ae72a022c664b2de2e466ed0ad \
--hash=sha256:d081a1f3800f05409ed868ebb2d74ac39dd0c1ff6c035b5162356d76030736d4 \
--hash=sha256:d25e97bc1f5f8f7985bdc2335ef9e73843bb561eb1fa6831fdfc295c1c2061cf \
--hash=sha256:d34f950ae05a83e0ede899c595f312ca976023ea1db100cd5aa188f7005e3ab0 \
--hash=sha256:d405d14bea042f166512add3091c1af40437c2e7f86988f3915fabd27b1e9cd2 \
--hash=sha256:d72f2b5e6e82ab8f94ea7d0d42f83c487dc159c5240d8f83beae684472864e2d \
--hash=sha256:d95b253b88f7d308b1c0b417c4624f44553ba4762816f94e6986819b9c273fb2 \
--hash=sha256:de2cfbb09e88f0f795fd90cf955858fc2c691df65b1f21f0aa00b99f3fbc661d \
--hash=sha256:de7c42f897e689ee6f9e93c4bec72b99ae3b32a2ade1c7e4798e690ff5246e02 \
--hash=sha256:e6ab5ab30ef325b443f379ddb575a34969c333004fca5a1daa0133a6ffaad616 \
--hash=sha256:e7393f1d64792763a48924ba31d1e44c2cfbc05e3b1c2c9abb4ceeadd912cced \
--hash=sha256:e9205d97ed08a82ebb9a307e92914bb30e18cdf6f6b12ca4bedadb1588a0bfe1 \
--hash=sha256:ecde6dedd6fff127c273c76821bb754d793be1024bc33314a120f83a3c69460c \
--hash=sha256:ed97fd56a561f5eb5706cebe94f1ad7c13b84d98312a05546f2ad036bafe87f4 \
--hash=sha256:f52679ff4218d713b3b33f88c89ccbf3a5c2c12ba665fb80ccc4192b4608dbab \
--hash=sha256:f8e49c9c364a7edcbe2a310f12733aad95b022495ef2a8d653f645e5d20c1564 \
--hash=sha256:fc3b4cc4539e055cfa39a3763c939f9d409eb40e85813257dcd761985a108554
# via pydantic
python-dotenv==1.2.1 \
--hash=sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6 \
--hash=sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61
# via dotenv
rapidfuzz==3.14.1 \
--hash=sha256:01eab10ec90912d7d28b3f08f6c91adbaf93458a53f849ff70776ecd70dd7a7a \
--hash=sha256:0591df2e856ad583644b40a2b99fb522f93543c65e64b771241dda6d1cfdc96b \
--hash=sha256:2e3e61c9e80d8c26709d8aa5c51fdd25139c81a4ab463895f8a567f8347b0548 \
--hash=sha256:37017b84953927807847016620d61251fe236bd4bcb25e27b6133d955bb9cafb \
--hash=sha256:40301b93b99350edcd02dbb22e37ca5f2a75d0db822e9b3c522da451a93d6f27 \
--hash=sha256:40875e0c06f1a388f1cab3885744f847b557e0b1642dfc31ff02039f9f0823ef \
--hash=sha256:4373f914ff524ee0146919dea96a40a8200ab157e5a15e777a74a769f73d8a4a \
--hash=sha256:44e741d785de57d1a7bae03599c1cbc7335d0b060a35e60c44c382566e22782e \
--hash=sha256:474f416cbb9099676de54aa41944c154ba8d25033ee460f87bb23e54af6d01c9 \
--hash=sha256:4acc20776f225ee37d69517a237c090b9fa7e0836a0b8bc58868e9168ba6ef6f \
--hash=sha256:57047493a1f62f11354c7143c380b02f1b355c52733e6b03adb1cb0fe8fb8816 \
--hash=sha256:5967d571243cfb9ad3710e6e628ab68c421a237b76e24a67ac22ee0ff12784d6 \
--hash=sha256:60879fcae2f7618403c4c746a9a3eec89327d73148fb6e89a933b78442ff0669 \
--hash=sha256:61458e83b0b3e2abc3391d0953c47d6325e506ba44d6a25c869c4401b3bc222c \
--hash=sha256:61d77e09b2b6bc38228f53b9ea7972a00722a14a6048be9a3672fb5cb08bad3a \
--hash=sha256:67ea46fa8cc78174bad09d66b9a4b98d3068e85de677e3c71ed931a1de28171f \
--hash=sha256:6cb56b695421538fdbe2c0c85888b991d833b8637d2f2b41faa79cea7234c000 \
--hash=sha256:6f571d20152fc4833b7b5e781b36d5e4f31f3b5a596a3d53cf66a1bd4436b4f4 \
--hash=sha256:7a2d80cc1a4fcc7e259ed4f505e70b36433a63fa251f1bb69ff279fe376c5efd \
--hash=sha256:7cd312c380d3ce9d35c3ec9726b75eee9da50e8a38e89e229a03db2262d3d96b \
--hash=sha256:83b8cc6336709fa5db0579189bfd125df280a554af544b2dc1c7da9cdad7e44d \
--hash=sha256:876dc0c15552f3d704d7fb8d61bdffc872ff63bedf683568d6faad32e51bbce8 \
--hash=sha256:893fdfd4f66ebb67f33da89eb1bd1674b7b30442fdee84db87f6cb9074bf0ce9 \
--hash=sha256:8b41d95ef86a6295d353dc3bb6c80550665ba2c3bef3a9feab46074d12a9af8f \
--hash=sha256:8d69f470d63ee824132ecd80b1974e1d15dd9df5193916901d7860cef081a260 \
--hash=sha256:93b6294a3ffab32a9b5f9b5ca048fa0474998e7e8bb0f2d2b5e819c64cb71ec7 \
--hash=sha256:ace21f7a78519d8e889b1240489cd021c5355c496cb151b479b741a4c27f0a25 \
--hash=sha256:ae2d57464b59297f727c4e201ea99ec7b13935f1f056c753e8103da3f2fc2404 \
--hash=sha256:b02850e7f7152bd1edff27e9d584505b84968cacedee7a734ec4050c655a803c \
--hash=sha256:b1fe6001baa9fa36bcb565e24e88830718f6c90896b91ceffcb48881e3adddbc \
--hash=sha256:c8d1dd1146539e093b84d0805e8951475644af794ace81d957ca612e3eb31598 \
--hash=sha256:cb5acf24590bc5e57027283b015950d713f9e4d155fda5cfa71adef3b3a84502 \
--hash=sha256:cf75769662eadf5f9bd24e865c19e5ca7718e879273dce4e7b3b5824c4da0eb4 \
--hash=sha256:d937dbeda71c921ef6537c6d41a84f1b8112f107589c9977059de57a1d726dd6 \
--hash=sha256:da011a373722fac6e64687297a1d17dc8461b82cb12c437845d5a5b161bc24b9 \
--hash=sha256:e84d9a844dc2e4d5c4cabd14c096374ead006583304333c14a6fbde51f612a44 \
--hash=sha256:f277801f55b2f3923ef2de51ab94689a0671a4524bf7b611de979f308a54cd6f \
--hash=sha256:f51c7571295ea97387bac4f048d73cecce51222be78ed808263b45c79c40a440 \
--hash=sha256:f94d61e44db3fc95a74006a394257af90fa6e826c900a501d749979ff495d702 \
--hash=sha256:fe2651258c1f1afa9b66f44bf82f639d5f83034f9804877a1bbbae2120539ad1 \
--hash=sha256:fedd5097a44808dddf341466866e5c57a18a19a336565b4ff50aa8f09eb528f6
# via input-to-route
requests==2.32.5 \
--hash=sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6 \
--hash=sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf
# via input-to-route
sniffio==1.3.1 \
--hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \
--hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc
# via
# anyio
# openai
starlette==0.49.1 \
--hash=sha256:481a43b71e24ed8c43b11ea02f5353d77840e01480881b8cb5a26b8cae64a8cb \
--hash=sha256:d92ce9f07e4a3caa3ac13a79523bd18e3bc0042bb8ff2d759a8e7dd0e1859875
# via fastapi
tqdm==4.67.1 \
--hash=sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2 \
--hash=sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2
# via openai
typing-extensions==4.15.0 \
--hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \
--hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548
# via
# fastapi
# openai
# pydantic
# pydantic-core
# typing-inspection
typing-inspection==0.4.2 \
--hash=sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7 \
--hash=sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464
# via pydantic
urllib3==2.5.0 \
--hash=sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760 \
--hash=sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc
# via requests
uvicorn==0.38.0 \
--hash=sha256:48c0afd214ceb59340075b4a052ea1ee91c16fbc2a9b1469cca0e54566977b02 \
--hash=sha256:fd97093bdd120a2609fc0d3afe931d4d4ad688b6e75f0f929fde1bc36fe0e91d
# via input-to-route

470
input-to-route/route.py Normal file
View File

@@ -0,0 +1,470 @@
import math
import itertools
import requests
from typing import List, Tuple, Dict, Optional, Set
import random
class Point:
def __init__(self, coord: List[float], tag: str, visit_time: int, list_id=0):
self.coord = coord
self.tag = tag
self.visit_time = visit_time
self.matrix_index = None # Индекс точки в матрице расстояний
self.estimated_time = None # Оценочное время (перемещение + посещение)
self.list_id = list_id
def haversine(coord1: List[float], coord2: List[float]) -> float:
"""Calculate the great-circle distance between two points in kilometers."""
lat1, lon1 = coord1
lat2, lon2 = coord2
R = 6371 # Earth radius in km
dlat = math.radians(lat2 - lat1)
dlon = math.radians(lon2 - lon1)
a = (math.sin(dlat/2) * math.sin(dlat/2) +
math.cos(math.radians(lat1)) * math.cos(math.radians(lat2)) *
math.sin(dlon/2) * math.sin(dlon/2))
return R * 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
def filter_points_by_time(start_coord: List[float], points: List[Point], total_time: int) -> List[Point]:
"""Filter points based on straight-line distance and visit time."""
filtered = []
for point in points:
distance = haversine(start_coord, point.coord)
travel_time = distance * 10 # Assume 6 km/h walking speed (10 min/km)
point.estimated_time = travel_time + point.visit_time
if point.estimated_time <= total_time:
filtered.append(point)
return filtered
def filter_points_by_tag_proximity(points: List[Point], max_per_tag: int = 10) -> List[Point]:
"""For each tag, keep only the closest points (by estimated time)."""
# Group points by tag
tag_to_points = {}
for point in points:
if point.tag not in tag_to_points:
tag_to_points[point.tag] = []
tag_to_points[point.tag].append(point)
# For each tag, sort by estimated time and keep top max_per_tag
filtered_points = []
for tag, tag_points in tag_to_points.items():
# Sort by estimated time (ascending)
sorted_points = sorted(tag_points, key=lambda p: p.estimated_time)
# Keep at most max_per_tag points
kept_points = sorted_points[:max_per_tag]
filtered_points.extend(kept_points)
print(f"Tag '{tag}': kept {len(kept_points)} out of {len(tag_points)} points")
return filtered_points
def get_duration_matrix(points: List[List[float]]) -> Optional[Tuple[List[List[float]], List[List[float]]]]:
"""Get duration matrix from server."""
url = "https://ha1m-maap-pdmc.gw-1a.dockhost.net/table"
payload = {"points": points}
headers = {"content-type": "application/json"}
try:
response = requests.post(url, json=payload, headers=headers, timeout=30)
if response.status_code == 200:
data = response.json()
return data.get("distances"), data.get("durations")
else:
print(f"Server error: {response.status_code}")
return None
except Exception as e:
print(f"Error requesting duration matrix: {e}")
return None
def group_points_by_significance(points: List[Point], tag_importance: Dict[str, int]) -> Dict[int, List[Point]]:
"""Group points by their importance level."""
grouped = {}
for point in points:
importance = tag_importance.get(point.tag, float('inf'))[0]
if importance not in grouped:
grouped[importance] = []
grouped[importance].append(point)
return grouped
def calculate_route_time_with_matrix(route: List[Point], start_coord: List[float],
duration_matrix: List[List[float]]) -> float:
"""Calculate total time for a route using the duration matrix."""
total_time = 0
current_index = 0 # Start point index
for point in route:
next_index = point.matrix_index
travel_time_seconds = duration_matrix[current_index][next_index]
travel_time_minutes = travel_time_seconds / 60.0
total_time += travel_time_minutes + point.visit_time
current_index = next_index
return total_time
def check_tags_constraint(points: List[Point]) -> bool:
"""Check if there are no more than 5 unique tags."""
unique_tags = set(point.tag for point in points)
return len(unique_tags) <= 5
def generate_routes_exact_tags(grouped_points: Dict[int, List[Point]],
all_tags: Set[str],
tag_importance: Dict[str, int]) -> List[List[Point]]:
"""Generate routes where each tag is visited exactly once using different coordinates."""
# Create a mapping from tag to points
tag_to_points = {}
for points_list in grouped_points.values():
for point in points_list:
if point.tag not in tag_to_points:
tag_to_points[point.tag] = []
tag_to_points[point.tag].append(point)
# For each tag, we need to select exactly one point
tag_selections = []
for tag in all_tags:
tag_selections.append(tag_to_points[tag])
# Generate all combinations of points (one per tag)
all_routes = []
print(len(list(itertools.product(*tag_selections))))
for point_combination in itertools.product(*tag_selections):
# Check if all points have unique coordinates
coords = [tuple(point.coord) for point in point_combination]
if len(coords) != len(set(coords)):
continue # Skip if any coordinates are duplicated
# Group points by importance
points_by_importance = {}
for point in point_combination:
imp = tag_importance[point.tag][0]
if imp not in points_by_importance:
points_by_importance[imp] = []
points_by_importance[imp].append(point)
# Sort by importance
sorted_importances = sorted(points_by_importance.keys())
# Generate all permutations within each importance group
importance_groups = [points_by_importance[imp] for imp in sorted_importances]
for ordering in itertools.product(*[itertools.permutations(group) for group in importance_groups]):
route = []
for group in ordering:
route.extend(group)
all_routes.append(route)
return all_routes
def generate_routes_with_repeats(grouped_points: Dict[int, List[Point]],
all_tags: Set[str],
tag_importance: Dict[str, int],
num_points: int) -> List[List[Point]]:
"""Generate routes when we need to repeat tags to reach the required number of points, ensuring unique coordinates."""
# Create a mapping from tag to points
tag_to_points = {}
for points_list in grouped_points.values():
for point in points_list:
if point.tag not in tag_to_points:
tag_to_points[point.tag] = []
tag_to_points[point.tag].append(point)
all_routes = []
# First, select one point for each tag (mandatory points)
mandatory_selections = [tag_to_points[tag] for tag in all_tags]
# Generate all combinations of mandatory points (one per tag)
for mandatory_combo in itertools.product(*mandatory_selections):
mandatory_points = list(mandatory_combo)
# Check if mandatory points have unique coordinates
mandatory_coords = [tuple(point.coord) for point in mandatory_points]
if len(mandatory_coords) != len(set(mandatory_coords)):
continue # Skip if any coordinates are duplicated in mandatory points
# We need to add (num_points - len(mandatory_points)) additional points
num_additional = num_points - len(mandatory_points)
if num_additional == 0:
# We have exactly the right number of points
points_by_importance = {}
for point in mandatory_points:
imp = tag_importance[point.tag][0]
if imp not in points_by_importance:
points_by_importance[imp] = []
points_by_importance[imp].append(point)
sorted_importances = sorted(points_by_importance.keys())
importance_groups = [points_by_importance[imp] for imp in sorted_importances]
for ordering in itertools.product(*[itertools.permutations(group) for group in importance_groups]):
route = []
for group in ordering:
route.extend(group)
all_routes.append(route)
else:
# We need to add additional points (can be from any tag, including repeats)
# But we must ensure all coordinates are unique
# Get all available points excluding mandatory points
all_available_points = []
for key in grouped_points.keys():
if tag_importance[key][1]:
all_available_points.extend(grouped_points[key])
# Remove mandatory points from available points
available_points = [p for p in all_available_points if p not in mandatory_points]
# Generate combinations of additional points
for additional_combo in itertools.combinations(available_points, num_additional):
# Check if additional points have unique coordinates and don't duplicate with mandatory
additional_coords = [tuple(point.coord) for point in additional_combo]
if len(additional_coords) != len(set(additional_coords)):
continue # Skip if any coordinates are duplicated in additional points
# Check if additional points don't duplicate with mandatory points
all_coords = mandatory_coords + additional_coords
if len(all_coords) != len(set(all_coords)):
continue # Skip if any coordinates are duplicated between mandatory and additional
full_route_candidate = mandatory_points + list(additional_combo)
# Group by importance
points_by_importance = {}
for point in full_route_candidate:
imp = tag_importance[point.tag]
if imp not in points_by_importance:
points_by_importance[imp] = []
points_by_importance[imp].append(point)
sorted_importances = sorted(points_by_importance.keys())
importance_groups = [points_by_importance[imp] for imp in sorted_importances]
for ordering in itertools.product(*[itertools.permutations(group) for group in importance_groups]):
route = []
for group in ordering:
route.extend(group)
all_routes.append(route)
return all_routes
def form_point_list(data):
point_list =[]
for list_id,entry in enumerate(data):
point = Point(list(map(float,entry['coordinate'].split(', '))),entry['type'],entry['time_to_visit'],list_id)
point_list.append(point)
return point_list
def build_route(data, mapping,start_coord,total_time,n_nodes,strategy='best'):
# Example input data - теперь с не более чем 5 уникальными тегами
start_coord_test = [56.331576, 44.003277]
total_time_test = 180 # Увеличим время до 4 часов для большего выбора
points = form_point_list(data)
tag_importance =mapping
# Используем 3 уникальных тега для демонстрации
points_test = [
Point([56.32448, 43.983546], "Памятник", 20),
Point([56.335607, 43.97481], "Архитектура", 20),
Point([56.313472, 43.990747], "Памятник", 20),
#Point([56.324157, 44.002696], "Памятник", 20),
#Point([56.316436, 43.994177], "Памятник", 20),
#Point([56.32377, 44.001879], "Памятник", 20),
#Point([56.329867, 43.99687], "Памятник", 20),
Point([56.311066, 43.94595], "Памятник", 20),
Point([56.333265, 43.972417], "Памятник", 20),
# Point([56.332166, 44.012111], "Памятник", 20),
#Point([56.326786, 44.006836], "Памятник", 20),
Point([56.330232, 44.010941], "Парк", 20),
Point([56.282221, 43.979263], "Парк", 20),
Point([56.277315, 43.921408], "Мозаика", 20),
Point([56.284829, 44.01893], "Парк", 20),
Point([56.308973, 43.99821], "Парк", 20),
Point([56.321545, 44.001921], "Парк", 20),
#Point([56.301798, 44.044003], "Мозаика", 20),
Point([56.268282, 43.919475], "Парк", 20),
Point([56.239625, 43.854551], "Парк", 20),
#Point([56.311214, 43.933981], "Парк", 20),
Point([56.314984, 44.007347], "Парк", 20),
Point([56.32509, 43.983433], "Парк", 20),
Point([56.27449, 43.973357], "Парк", 20),
Point([56.278073, 43.940886], "Парк", 20),
Point([56.358805, 43.825376], "Парк", 20),
Point([56.329995, 44.009444], "Памятник", 20),
Point([56.328551, 43.998718], "Памятник", 20),
Point([56.330355, 43.993105], "Архитектура", 20),
Point([56.321416, 43.973897], "Архитектура", 20),
# Point([56.327298, 44.005706], "Архитектура", 20),
#Point([56.328757, 43.998183], "Архитектура", 20),
# Point([56.328908, 43.995645], "Архитектура", 20),
Point([56.317578, 43.995805], "Архитектура", 20),
Point([56.329433, 44.012764], "Архитектура", 20),
Point([56.3301, 44.008831], "Архитектура", 20),
#Point([56.32995, 43.999495], "Архитектура", 20),
Point([56.327454, 44.041745], "Архитектура", 20),
#Point([56.328576, 44.004872], "Архитектура", 20),
Point([56.3275, 44.007658], "Архитектура", 20),
Point([56.330679, 44.013874], "Архитектура", 20),
# Point([56.331541, 44.001747], "Архитектура", 20),
# Point([56.335071, 43.974627], "Архитектура", 20),
#Point([56.317707, 43.995847], "Архитектура", 20),
#Point([56.323851, 43.985939], "Архитектура", 20),
Point([56.325701, 44.001527], "Архитектура", 20),
Point([56.328754, 43.998954], "Архитектура", 20),
#Point([56.323937, 43.990728], "Музей", 20),
#Point([56.2841, 43.84621], "Музей", 20),
#Point([56.328646, 44.028973], "Музей", 20),
Point([56.327391, 43.857522], "Мозаика", 20),
#Point([56.252239, 43.889066], "Мозаика", 20),
#Point([56.248436, 43.88106], "Мозаика", 20),
#Point([56.321257, 43.94545], "Мозаика", 20),
# Point([56.365284, 43.823251], "Мозаика", 20),
Point([56.294371, 43.912625], "Мозаика", 20),
#Point([56.241768, 43.859687], "Мозаика", 20),
#Point([56.300073, 43.938526], "Мозаика", 20),
#Point([56.229652, 43.947973], "Мозаика", 20),
# Point([56.269486, 43.9238], "Мозаика", 20),
Point([56.299251, 43.985146], "Мозаика", 20),
Point([56.293297, 44.034095], "Мозаика", 20),
Point([56.299251, 43.985146], "Мозаика", 20),
Point([56.229652, 43.947973], "Мозаика", 20),
Point([56.269486, 43.9238], "Мозаика", 20),
#Point([56.293297, 44.034095], "Мозаика", 20),
#Point([56.229652, 43.947973], "Мозаика", 20)
]
tag_importance_test = {
"Памятник": 1,
"Парк": 1,
"Мозаика": 1,
"Архитектура": 1,
#"Музей": 1
}
# Check tags constraint
if not check_tags_constraint(points):
print("Error: More than 5 unique tags in the input data")
return
print("Input data validation: OK (5 or fewer unique tags)")
# Step 1: Filter points using straight-line distance and total time
filtered_by_time = filter_points_by_time(start_coord, points, total_time)
print(f"After initial time filtering: {len(filtered_by_time)} points")
if len(filtered_by_time) < 3:
print("Not enough points after time filtering")
return
# Step 2: Filter points by tag proximity (keep max 10 closest points per tag)
filtered_points = filter_points_by_tag_proximity(filtered_by_time, max_per_tag=10)
print(f"After tag proximity filtering: {len(filtered_points)} points")
if len(filtered_points) < 3:
print("Not enough points after tag proximity filtering")
return
# Step 3: Prepare points for server request (start point + filtered points)
points_for_matrix = [start_coord] + [point.coord for point in filtered_points]
print("Requesting duration matrix from server...")
# Step 4: Get duration matrix from server
result = get_duration_matrix(points_for_matrix)
if result is None:
print("Failed to get duration matrix from server")
return
distances_matrix, durations_matrix = result
print("Duration matrix received successfully")
# Assign matrix indices to points
for i, point in enumerate(filtered_points):
point.matrix_index = i + 1 # +1 because index 0 is the start point
# Step 5: Group by importance
grouped_points = group_points_by_significance(filtered_points, tag_importance)
# Get all unique tags
all_tags = set(point.tag for point in filtered_points)
num_unique_tags = len(all_tags)
print(f"Unique tags: {all_tags} ({num_unique_tags} tags)")
# Step 6: Generate possible routes
print("Generating possible routes...")
# Determine the number of points in the route
if num_unique_tags >= n_nodes:
# Each tag must be visited exactly once
print("Each tag will be visited exactly once with unique coordinates")
possible_routes = generate_routes_exact_tags(grouped_points, all_tags, tag_importance)
else:
# We have fewer than 3 unique tags, need to repeat some tags
print(f"Only {num_unique_tags} unique tags available, will repeat tags to reach 3 points with unique coordinates")
possible_routes = generate_routes_with_repeats(grouped_points, all_tags, tag_importance, n_nodes)
if not possible_routes:
print("No valid routes found that cover all tags with unique coordinates")
return
# Step 7: Calculate time for each route and filter by total_time
valid_routes = []
for route in possible_routes:
route_time = calculate_route_time_with_matrix(route, start_coord, durations_matrix)
if route_time <= total_time:
valid_routes.append((route, route_time))
if not valid_routes:
print("No valid routes found within time constraint")
return
# Step 8: Find optimal route (minimum time)
if strategy=='random':
optimal_route, min_time = random.choice(valid_routes)
elif strategy=='longest':
optimal_route, min_time = max(valid_routes, key=lambda x: x[1])
else:
optimal_route, min_time = min(valid_routes, key=lambda x: x[1])
print(f"\nOptimal route (time: {min_time:.2f} min):")
for i, point in enumerate(optimal_route, 1):
print(f"{i}. {point.tag} at {point.coord} ({point.visit_time} min)")
# Print route details with travel times
print("\nRoute details:")
current_index = 0
total_route_time = 0
for i, point in enumerate(optimal_route):
travel_time_seconds = durations_matrix[current_index][point.matrix_index]
travel_time_minutes = travel_time_seconds / 60.0
segment_time = travel_time_minutes + point.visit_time
total_route_time += segment_time
point.time_to_travel = travel_time_minutes
print(f"Segment {i+1}: {travel_time_minutes:.2f} min travel + {point.visit_time} min visit = {segment_time:.2f} min")
current_index = point.matrix_index
print(f"Total route time: {total_route_time:.2f} min")
# Display all tags covered by the route
route_tags = set(point.tag for point in optimal_route)
#print(f"\nTags covered in this route: {', '.join(route_tags)}")
if all_tags.issubset(route_tags):
print("All tags are covered in this route!")
# Verify all coordinates are unique
route_coords = [tuple(point.coord) for point in optimal_route]
if len(route_coords) == len(set(route_coords)):
print("All coordinates in the route are unique!")
else:
print("ERROR: Duplicate coordinates found in the route!")
places = []
for point in optimal_route:
place = data[point.list_id].copy()
place["time_to_travel"] = round(point.time_to_travel, 2)
places.append(place)
return route_coords, places
#if __name__ == "__main__":
# build_route()

649
input-to-route/uv.lock generated Normal file
View File

@@ -0,0 +1,649 @@
version = 1
revision = 2
requires-python = ">=3.13"
[[package]]
name = "annotated-doc"
version = "0.0.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d7/a6/dc46877b911e40c00d395771ea710d5e77b6de7bacd5fdcd78d70cc5a48f/annotated_doc-0.0.3.tar.gz", hash = "sha256:e18370014c70187422c33e945053ff4c286f453a984eba84d0dbfa0c935adeda", size = 5535, upload-time = "2025-10-24T14:57:10.718Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/02/b7/cf592cb5de5cb3bade3357f8d2cf42bf103bbe39f459824b4939fd212911/annotated_doc-0.0.3-py3-none-any.whl", hash = "sha256:348ec6664a76f1fd3be81f43dffbee4c7e8ce931ba71ec67cc7f4ade7fbbb580", size = 5488, upload-time = "2025-10-24T14:57:09.462Z" },
]
[[package]]
name = "annotated-types"
version = "0.7.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
]
[[package]]
name = "anyio"
version = "4.11.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "idna" },
{ name = "sniffio" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c6/78/7d432127c41b50bccba979505f272c16cbcadcc33645d5fa3a738110ae75/anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4", size = 219094, upload-time = "2025-09-23T09:19:12.58Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097, upload-time = "2025-09-23T09:19:10.601Z" },
]
[[package]]
name = "certifi"
version = "2025.10.5"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/4c/5b/b6ce21586237c77ce67d01dc5507039d444b630dd76611bbca2d8e5dcd91/certifi-2025.10.5.tar.gz", hash = "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43", size = 164519, upload-time = "2025-10-05T04:12:15.808Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de", size = 163286, upload-time = "2025-10-05T04:12:14.03Z" },
]
[[package]]
name = "charset-normalizer"
version = "3.4.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" },
{ url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" },
{ url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" },
{ url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" },
{ url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" },
{ url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" },
{ url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" },
{ url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" },
{ url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" },
{ url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" },
{ url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" },
{ url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" },
{ url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" },
{ url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" },
{ url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" },
{ url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" },
{ url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" },
{ url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" },
{ url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" },
{ url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" },
{ url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" },
{ url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" },
{ url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" },
{ url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" },
{ url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" },
{ url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" },
{ url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" },
{ url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" },
{ url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" },
{ url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" },
{ url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" },
{ url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" },
{ url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" },
]
[[package]]
name = "click"
version = "8.3.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/46/61/de6cd827efad202d7057d93e0fed9294b96952e188f7384832791c7b2254/click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4", size = 276943, upload-time = "2025-09-18T17:32:23.696Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", size = 107295, upload-time = "2025-09-18T17:32:22.42Z" },
]
[[package]]
name = "colorama"
version = "0.4.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
]
[[package]]
name = "distro"
version = "1.9.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" },
]
[[package]]
name = "dotenv"
version = "0.9.9"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "python-dotenv" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/b2/b7/545d2c10c1fc15e48653c91efde329a790f2eecfbbf2bd16003b5db2bab0/dotenv-0.9.9-py2.py3-none-any.whl", hash = "sha256:29cf74a087b31dafdb5a446b6d7e11cbce8ed2741540e2339c69fbef92c94ce9", size = 1892, upload-time = "2025-02-19T22:15:01.647Z" },
]
[[package]]
name = "et-xmlfile"
version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d3/38/af70d7ab1ae9d4da450eeec1fa3918940a5fafb9055e934af8d6eb0c2313/et_xmlfile-2.0.0.tar.gz", hash = "sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54", size = 17234, upload-time = "2024-10-25T17:25:40.039Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c1/8b/5fe2cc11fee489817272089c4203e679c63b570a5aaeb18d852ae3cbba6a/et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa", size = 18059, upload-time = "2024-10-25T17:25:39.051Z" },
]
[[package]]
name = "fastapi"
version = "0.120.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "annotated-doc" },
{ name = "pydantic" },
{ name = "starlette" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a0/fb/79e556bc8f9d360e5cc2fa7364a7ad6bda6f1736938b43a2791fa8baee7b/fastapi-0.120.2.tar.gz", hash = "sha256:4c5ab43e2a90335bbd8326d1b659eac0f3dbcc015e2af573c4f5de406232c4ac", size = 338684, upload-time = "2025-10-29T13:47:35.802Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/81/cc/1c33d05f62c9349bb80dfe789cc9a7409bdfb337a63fa347fd651d25294a/fastapi-0.120.2-py3-none-any.whl", hash = "sha256:bedcf2c14240e43d56cb9a339b32bcf15104fe6b5897c0222603cb7ec416c8eb", size = 108383, upload-time = "2025-10-29T13:47:32.978Z" },
]
[[package]]
name = "h11"
version = "0.16.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
]
[[package]]
name = "httpcore"
version = "1.0.9"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "h11" },
]
sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
]
[[package]]
name = "httpx"
version = "0.28.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "certifi" },
{ name = "httpcore" },
{ name = "idna" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
]
[[package]]
name = "idna"
version = "3.11"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
]
[[package]]
name = "input-to-route"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "dotenv" },
{ name = "fastapi" },
{ name = "openai" },
{ name = "openpyxl" },
{ name = "pandas" },
{ name = "pydantic" },
{ name = "rapidfuzz" },
{ name = "requests" },
{ name = "uvicorn" },
]
[package.metadata]
requires-dist = [
{ name = "dotenv", specifier = ">=0.9.9" },
{ name = "fastapi", specifier = ">=0.120.2" },
{ name = "openai", specifier = ">=2.6.1" },
{ name = "openpyxl", specifier = ">=3.1.5" },
{ name = "pandas", specifier = ">=2.3.3" },
{ name = "pydantic", specifier = ">=2.12.3" },
{ name = "rapidfuzz", specifier = ">=3.14.1" },
{ name = "requests", specifier = ">=2.32.5" },
{ name = "uvicorn", specifier = ">=0.38.0" },
]
[[package]]
name = "jiter"
version = "0.11.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a3/68/0357982493a7b20925aece061f7fb7a2678e3b232f8d73a6edb7e5304443/jiter-0.11.1.tar.gz", hash = "sha256:849dcfc76481c0ea0099391235b7ca97d7279e0fa4c86005457ac7c88e8b76dc", size = 168385, upload-time = "2025-10-17T11:31:15.186Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7c/4b/e4dd3c76424fad02a601d570f4f2a8438daea47ba081201a721a903d3f4c/jiter-0.11.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:71b6a920a5550f057d49d0e8bcc60945a8da998019e83f01adf110e226267663", size = 305272, upload-time = "2025-10-17T11:29:39.249Z" },
{ url = "https://files.pythonhosted.org/packages/67/83/2cd3ad5364191130f4de80eacc907f693723beaab11a46c7d155b07a092c/jiter-0.11.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0b3de72e925388453a5171be83379549300db01284f04d2a6f244d1d8de36f94", size = 314038, upload-time = "2025-10-17T11:29:40.563Z" },
{ url = "https://files.pythonhosted.org/packages/d3/3c/8e67d9ba524e97d2f04c8f406f8769a23205026b13b0938d16646d6e2d3e/jiter-0.11.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc19dd65a2bd3d9c044c5b4ebf657ca1e6003a97c0fc10f555aa4f7fb9821c00", size = 345977, upload-time = "2025-10-17T11:29:42.009Z" },
{ url = "https://files.pythonhosted.org/packages/8d/a5/489ce64d992c29bccbffabb13961bbb0435e890d7f2d266d1f3df5e917d2/jiter-0.11.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d58faaa936743cd1464540562f60b7ce4fd927e695e8bc31b3da5b914baa9abd", size = 364503, upload-time = "2025-10-17T11:29:43.459Z" },
{ url = "https://files.pythonhosted.org/packages/d4/c0/e321dd83ee231d05c8fe4b1a12caf1f0e8c7a949bf4724d58397104f10f2/jiter-0.11.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:902640c3103625317291cb73773413b4d71847cdf9383ba65528745ff89f1d14", size = 487092, upload-time = "2025-10-17T11:29:44.835Z" },
{ url = "https://files.pythonhosted.org/packages/f9/5e/8f24ec49c8d37bd37f34ec0112e0b1a3b4b5a7b456c8efff1df5e189ad43/jiter-0.11.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:30405f726e4c2ed487b176c09f8b877a957f535d60c1bf194abb8dadedb5836f", size = 376328, upload-time = "2025-10-17T11:29:46.175Z" },
{ url = "https://files.pythonhosted.org/packages/7f/70/ded107620e809327cf7050727e17ccfa79d6385a771b7fe38fb31318ef00/jiter-0.11.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3217f61728b0baadd2551844870f65219ac4a1285d5e1a4abddff3d51fdabe96", size = 356632, upload-time = "2025-10-17T11:29:47.454Z" },
{ url = "https://files.pythonhosted.org/packages/19/53/c26f7251613f6a9079275ee43c89b8a973a95ff27532c421abc2a87afb04/jiter-0.11.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b1364cc90c03a8196f35f396f84029f12abe925415049204446db86598c8b72c", size = 384358, upload-time = "2025-10-17T11:29:49.377Z" },
{ url = "https://files.pythonhosted.org/packages/84/16/e0f2cc61e9c4d0b62f6c1bd9b9781d878a427656f88293e2a5335fa8ff07/jiter-0.11.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:53a54bf8e873820ab186b2dca9f6c3303f00d65ae5e7b7d6bda1b95aa472d646", size = 517279, upload-time = "2025-10-17T11:29:50.968Z" },
{ url = "https://files.pythonhosted.org/packages/60/5c/4cd095eaee68961bca3081acbe7c89e12ae24a5dae5fd5d2a13e01ed2542/jiter-0.11.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:7e29aca023627b0e0c2392d4248f6414d566ff3974fa08ff2ac8dbb96dfee92a", size = 508276, upload-time = "2025-10-17T11:29:52.619Z" },
{ url = "https://files.pythonhosted.org/packages/4f/25/f459240e69b0e09a7706d96ce203ad615ca36b0fe832308d2b7123abf2d0/jiter-0.11.1-cp313-cp313-win32.whl", hash = "sha256:f153e31d8bca11363751e875c0a70b3d25160ecbaee7b51e457f14498fb39d8b", size = 205593, upload-time = "2025-10-17T11:29:53.938Z" },
{ url = "https://files.pythonhosted.org/packages/7c/16/461bafe22bae79bab74e217a09c907481a46d520c36b7b9fe71ee8c9e983/jiter-0.11.1-cp313-cp313-win_amd64.whl", hash = "sha256:f773f84080b667c69c4ea0403fc67bb08b07e2b7ce1ef335dea5868451e60fed", size = 203518, upload-time = "2025-10-17T11:29:55.216Z" },
{ url = "https://files.pythonhosted.org/packages/7b/72/c45de6e320edb4fa165b7b1a414193b3cae302dd82da2169d315dcc78b44/jiter-0.11.1-cp313-cp313-win_arm64.whl", hash = "sha256:635ecd45c04e4c340d2187bcb1cea204c7cc9d32c1364d251564bf42e0e39c2d", size = 188062, upload-time = "2025-10-17T11:29:56.631Z" },
{ url = "https://files.pythonhosted.org/packages/65/9b/4a57922437ca8753ef823f434c2dec5028b237d84fa320f06a3ba1aec6e8/jiter-0.11.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d892b184da4d94d94ddb4031296931c74ec8b325513a541ebfd6dfb9ae89904b", size = 313814, upload-time = "2025-10-17T11:29:58.509Z" },
{ url = "https://files.pythonhosted.org/packages/76/50/62a0683dadca25490a4bedc6a88d59de9af2a3406dd5a576009a73a1d392/jiter-0.11.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa22c223a3041dacb2fcd37c70dfd648b44662b4a48e242592f95bda5ab09d58", size = 344987, upload-time = "2025-10-17T11:30:00.208Z" },
{ url = "https://files.pythonhosted.org/packages/da/00/2355dbfcbf6cdeaddfdca18287f0f38ae49446bb6378e4a5971e9356fc8a/jiter-0.11.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:330e8e6a11ad4980cd66a0f4a3e0e2e0f646c911ce047014f984841924729789", size = 356399, upload-time = "2025-10-17T11:30:02.084Z" },
{ url = "https://files.pythonhosted.org/packages/c9/07/c2bd748d578fa933d894a55bff33f983bc27f75fc4e491b354bef7b78012/jiter-0.11.1-cp313-cp313t-win_amd64.whl", hash = "sha256:09e2e386ebf298547ca3a3704b729471f7ec666c2906c5c26c1a915ea24741ec", size = 203289, upload-time = "2025-10-17T11:30:03.656Z" },
{ url = "https://files.pythonhosted.org/packages/e6/ee/ace64a853a1acbd318eb0ca167bad1cf5ee037207504b83a868a5849747b/jiter-0.11.1-cp313-cp313t-win_arm64.whl", hash = "sha256:fe4a431c291157e11cee7c34627990ea75e8d153894365a3bc84b7a959d23ca8", size = 188284, upload-time = "2025-10-17T11:30:05.046Z" },
{ url = "https://files.pythonhosted.org/packages/8d/00/d6006d069e7b076e4c66af90656b63da9481954f290d5eca8c715f4bf125/jiter-0.11.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:0fa1f70da7a8a9713ff8e5f75ec3f90c0c870be6d526aa95e7c906f6a1c8c676", size = 304624, upload-time = "2025-10-17T11:30:06.678Z" },
{ url = "https://files.pythonhosted.org/packages/fc/45/4a0e31eb996b9ccfddbae4d3017b46f358a599ccf2e19fbffa5e531bd304/jiter-0.11.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:569ee559e5046a42feb6828c55307cf20fe43308e3ae0d8e9e4f8d8634d99944", size = 315042, upload-time = "2025-10-17T11:30:08.87Z" },
{ url = "https://files.pythonhosted.org/packages/e7/91/22f5746f5159a28c76acdc0778801f3c1181799aab196dbea2d29e064968/jiter-0.11.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f69955fa1d92e81987f092b233f0be49d4c937da107b7f7dcf56306f1d3fcce9", size = 346357, upload-time = "2025-10-17T11:30:10.222Z" },
{ url = "https://files.pythonhosted.org/packages/f5/4f/57620857d4e1dc75c8ff4856c90cb6c135e61bff9b4ebfb5dc86814e82d7/jiter-0.11.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:090f4c9d4a825e0fcbd0a2647c9a88a0f366b75654d982d95a9590745ff0c48d", size = 365057, upload-time = "2025-10-17T11:30:11.585Z" },
{ url = "https://files.pythonhosted.org/packages/ce/34/caf7f9cc8ae0a5bb25a5440cc76c7452d264d1b36701b90fdadd28fe08ec/jiter-0.11.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bbf3d8cedf9e9d825233e0dcac28ff15c47b7c5512fdfe2e25fd5bbb6e6b0cee", size = 487086, upload-time = "2025-10-17T11:30:13.052Z" },
{ url = "https://files.pythonhosted.org/packages/50/17/85b5857c329d533d433fedf98804ebec696004a1f88cabad202b2ddc55cf/jiter-0.11.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2aa9b1958f9c30d3d1a558b75f0626733c60eb9b7774a86b34d88060be1e67fe", size = 376083, upload-time = "2025-10-17T11:30:14.416Z" },
{ url = "https://files.pythonhosted.org/packages/85/d3/2d9f973f828226e6faebdef034097a2918077ea776fb4d88489949024787/jiter-0.11.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e42d1ca16590b768c5e7d723055acd2633908baacb3628dd430842e2e035aa90", size = 357825, upload-time = "2025-10-17T11:30:15.765Z" },
{ url = "https://files.pythonhosted.org/packages/f4/55/848d4dabf2c2c236a05468c315c2cb9dc736c5915e65449ccecdba22fb6f/jiter-0.11.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5db4c2486a023820b701a17aec9c5a6173c5ba4393f26662f032f2de9c848b0f", size = 383933, upload-time = "2025-10-17T11:30:17.34Z" },
{ url = "https://files.pythonhosted.org/packages/0b/6c/204c95a4fbb0e26dfa7776c8ef4a878d0c0b215868011cc904bf44f707e2/jiter-0.11.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:4573b78777ccfac954859a6eff45cbd9d281d80c8af049d0f1a3d9fc323d5c3a", size = 517118, upload-time = "2025-10-17T11:30:18.684Z" },
{ url = "https://files.pythonhosted.org/packages/88/25/09956644ea5a2b1e7a2a0f665cb69a973b28f4621fa61fc0c0f06ff40a31/jiter-0.11.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:7593ac6f40831d7961cb67633c39b9fef6689a211d7919e958f45710504f52d3", size = 508194, upload-time = "2025-10-17T11:30:20.719Z" },
{ url = "https://files.pythonhosted.org/packages/09/49/4d1657355d7f5c9e783083a03a3f07d5858efa6916a7d9634d07db1c23bd/jiter-0.11.1-cp314-cp314-win32.whl", hash = "sha256:87202ec6ff9626ff5f9351507def98fcf0df60e9a146308e8ab221432228f4ea", size = 203961, upload-time = "2025-10-17T11:30:22.073Z" },
{ url = "https://files.pythonhosted.org/packages/76/bd/f063bd5cc2712e7ca3cf6beda50894418fc0cfeb3f6ff45a12d87af25996/jiter-0.11.1-cp314-cp314-win_amd64.whl", hash = "sha256:a5dd268f6531a182c89d0dd9a3f8848e86e92dfff4201b77a18e6b98aa59798c", size = 202804, upload-time = "2025-10-17T11:30:23.452Z" },
{ url = "https://files.pythonhosted.org/packages/52/ca/4d84193dfafef1020bf0bedd5e1a8d0e89cb67c54b8519040effc694964b/jiter-0.11.1-cp314-cp314-win_arm64.whl", hash = "sha256:5d761f863f912a44748a21b5c4979c04252588ded8d1d2760976d2e42cd8d991", size = 188001, upload-time = "2025-10-17T11:30:24.915Z" },
{ url = "https://files.pythonhosted.org/packages/d5/fa/3b05e5c9d32efc770a8510eeb0b071c42ae93a5b576fd91cee9af91689a1/jiter-0.11.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2cc5a3965285ddc33e0cab933e96b640bc9ba5940cea27ebbbf6695e72d6511c", size = 312561, upload-time = "2025-10-17T11:30:26.742Z" },
{ url = "https://files.pythonhosted.org/packages/50/d3/335822eb216154ddb79a130cbdce88fdf5c3e2b43dc5dba1fd95c485aaf5/jiter-0.11.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b572b3636a784c2768b2342f36a23078c8d3aa6d8a30745398b1bab58a6f1a8", size = 344551, upload-time = "2025-10-17T11:30:28.252Z" },
{ url = "https://files.pythonhosted.org/packages/31/6d/a0bed13676b1398f9b3ba61f32569f20a3ff270291161100956a577b2dd3/jiter-0.11.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ad93e3d67a981f96596d65d2298fe8d1aa649deb5374a2fb6a434410ee11915e", size = 363051, upload-time = "2025-10-17T11:30:30.009Z" },
{ url = "https://files.pythonhosted.org/packages/a4/03/313eda04aa08545a5a04ed5876e52f49ab76a4d98e54578896ca3e16313e/jiter-0.11.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a83097ce379e202dcc3fe3fc71a16d523d1ee9192c8e4e854158f96b3efe3f2f", size = 485897, upload-time = "2025-10-17T11:30:31.429Z" },
{ url = "https://files.pythonhosted.org/packages/5f/13/a1011b9d325e40b53b1b96a17c010b8646013417f3902f97a86325b19299/jiter-0.11.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7042c51e7fbeca65631eb0c332f90c0c082eab04334e7ccc28a8588e8e2804d9", size = 375224, upload-time = "2025-10-17T11:30:33.18Z" },
{ url = "https://files.pythonhosted.org/packages/92/da/1b45026b19dd39b419e917165ff0ea629dbb95f374a3a13d2df95e40a6ac/jiter-0.11.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a68d679c0e47649a61df591660507608adc2652442de7ec8276538ac46abe08", size = 356606, upload-time = "2025-10-17T11:30:34.572Z" },
{ url = "https://files.pythonhosted.org/packages/7a/0c/9acb0e54d6a8ba59ce923a180ebe824b4e00e80e56cefde86cc8e0a948be/jiter-0.11.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a1b0da75dbf4b6ec0b3c9e604d1ee8beaf15bc046fff7180f7d89e3cdbd3bb51", size = 384003, upload-time = "2025-10-17T11:30:35.987Z" },
{ url = "https://files.pythonhosted.org/packages/3f/2b/e5a5fe09d6da2145e4eed651e2ce37f3c0cf8016e48b1d302e21fb1628b7/jiter-0.11.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:69dd514bf0fa31c62147d6002e5ca2b3e7ef5894f5ac6f0a19752385f4e89437", size = 516946, upload-time = "2025-10-17T11:30:37.425Z" },
{ url = "https://files.pythonhosted.org/packages/5f/fe/db936e16e0228d48eb81f9934e8327e9fde5185e84f02174fcd22a01be87/jiter-0.11.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:bb31ac0b339efa24c0ca606febd8b77ef11c58d09af1b5f2be4c99e907b11111", size = 507614, upload-time = "2025-10-17T11:30:38.977Z" },
{ url = "https://files.pythonhosted.org/packages/86/db/c4438e8febfb303486d13c6b72f5eb71cf851e300a0c1f0b4140018dd31f/jiter-0.11.1-cp314-cp314t-win32.whl", hash = "sha256:b2ce0d6156a1d3ad41da3eec63b17e03e296b78b0e0da660876fccfada86d2f7", size = 204043, upload-time = "2025-10-17T11:30:40.308Z" },
{ url = "https://files.pythonhosted.org/packages/36/59/81badb169212f30f47f817dfaabf965bc9b8204fed906fab58104ee541f9/jiter-0.11.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f4db07d127b54c4a2d43b4cf05ff0193e4f73e0dd90c74037e16df0b29f666e1", size = 204046, upload-time = "2025-10-17T11:30:41.692Z" },
{ url = "https://files.pythonhosted.org/packages/dd/01/43f7b4eb61db3e565574c4c5714685d042fb652f9eef7e5a3de6aafa943a/jiter-0.11.1-cp314-cp314t-win_arm64.whl", hash = "sha256:28e4fdf2d7ebfc935523e50d1efa3970043cfaa161674fe66f9642409d001dfe", size = 188069, upload-time = "2025-10-17T11:30:43.23Z" },
]
[[package]]
name = "numpy"
version = "2.3.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b5/f4/098d2270d52b41f1bd7db9fc288aaa0400cb48c2a3e2af6fa365d9720947/numpy-2.3.4.tar.gz", hash = "sha256:a7d018bfedb375a8d979ac758b120ba846a7fe764911a64465fd87b8729f4a6a", size = 20582187, upload-time = "2025-10-15T16:18:11.77Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/57/7e/b72610cc91edf138bc588df5150957a4937221ca6058b825b4725c27be62/numpy-2.3.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c090d4860032b857d94144d1a9976b8e36709e40386db289aaf6672de2a81966", size = 20950335, upload-time = "2025-10-15T16:16:10.304Z" },
{ url = "https://files.pythonhosted.org/packages/3e/46/bdd3370dcea2f95ef14af79dbf81e6927102ddf1cc54adc0024d61252fd9/numpy-2.3.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a13fc473b6db0be619e45f11f9e81260f7302f8d180c49a22b6e6120022596b3", size = 14179878, upload-time = "2025-10-15T16:16:12.595Z" },
{ url = "https://files.pythonhosted.org/packages/ac/01/5a67cb785bda60f45415d09c2bc245433f1c68dd82eef9c9002c508b5a65/numpy-2.3.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:3634093d0b428e6c32c3a69b78e554f0cd20ee420dcad5a9f3b2a63762ce4197", size = 5108673, upload-time = "2025-10-15T16:16:14.877Z" },
{ url = "https://files.pythonhosted.org/packages/c2/cd/8428e23a9fcebd33988f4cb61208fda832800ca03781f471f3727a820704/numpy-2.3.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:043885b4f7e6e232d7df4f51ffdef8c36320ee9d5f227b380ea636722c7ed12e", size = 6641438, upload-time = "2025-10-15T16:16:16.805Z" },
{ url = "https://files.pythonhosted.org/packages/3e/d1/913fe563820f3c6b079f992458f7331278dcd7ba8427e8e745af37ddb44f/numpy-2.3.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4ee6a571d1e4f0ea6d5f22d6e5fbd6ed1dc2b18542848e1e7301bd190500c9d7", size = 14281290, upload-time = "2025-10-15T16:16:18.764Z" },
{ url = "https://files.pythonhosted.org/packages/9e/7e/7d306ff7cb143e6d975cfa7eb98a93e73495c4deabb7d1b5ecf09ea0fd69/numpy-2.3.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fc8a63918b04b8571789688b2780ab2b4a33ab44bfe8ccea36d3eba51228c953", size = 16636543, upload-time = "2025-10-15T16:16:21.072Z" },
{ url = "https://files.pythonhosted.org/packages/47/6a/8cfc486237e56ccfb0db234945552a557ca266f022d281a2f577b98e955c/numpy-2.3.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:40cc556d5abbc54aabe2b1ae287042d7bdb80c08edede19f0c0afb36ae586f37", size = 16056117, upload-time = "2025-10-15T16:16:23.369Z" },
{ url = "https://files.pythonhosted.org/packages/b1/0e/42cb5e69ea901e06ce24bfcc4b5664a56f950a70efdcf221f30d9615f3f3/numpy-2.3.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ecb63014bb7f4ce653f8be7f1df8cbc6093a5a2811211770f6606cc92b5a78fd", size = 18577788, upload-time = "2025-10-15T16:16:27.496Z" },
{ url = "https://files.pythonhosted.org/packages/86/92/41c3d5157d3177559ef0a35da50f0cda7fa071f4ba2306dd36818591a5bc/numpy-2.3.4-cp313-cp313-win32.whl", hash = "sha256:e8370eb6925bb8c1c4264fec52b0384b44f675f191df91cbe0140ec9f0955646", size = 6282620, upload-time = "2025-10-15T16:16:29.811Z" },
{ url = "https://files.pythonhosted.org/packages/09/97/fd421e8bc50766665ad35536c2bb4ef916533ba1fdd053a62d96cc7c8b95/numpy-2.3.4-cp313-cp313-win_amd64.whl", hash = "sha256:56209416e81a7893036eea03abcb91c130643eb14233b2515c90dcac963fe99d", size = 12784672, upload-time = "2025-10-15T16:16:31.589Z" },
{ url = "https://files.pythonhosted.org/packages/ad/df/5474fb2f74970ca8eb978093969b125a84cc3d30e47f82191f981f13a8a0/numpy-2.3.4-cp313-cp313-win_arm64.whl", hash = "sha256:a700a4031bc0fd6936e78a752eefb79092cecad2599ea9c8039c548bc097f9bc", size = 10196702, upload-time = "2025-10-15T16:16:33.902Z" },
{ url = "https://files.pythonhosted.org/packages/11/83/66ac031464ec1767ea3ed48ce40f615eb441072945e98693bec0bcd056cc/numpy-2.3.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:86966db35c4040fdca64f0816a1c1dd8dbd027d90fca5a57e00e1ca4cd41b879", size = 21049003, upload-time = "2025-10-15T16:16:36.101Z" },
{ url = "https://files.pythonhosted.org/packages/5f/99/5b14e0e686e61371659a1d5bebd04596b1d72227ce36eed121bb0aeab798/numpy-2.3.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:838f045478638b26c375ee96ea89464d38428c69170360b23a1a50fa4baa3562", size = 14302980, upload-time = "2025-10-15T16:16:39.124Z" },
{ url = "https://files.pythonhosted.org/packages/2c/44/e9486649cd087d9fc6920e3fc3ac2aba10838d10804b1e179fb7cbc4e634/numpy-2.3.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d7315ed1dab0286adca467377c8381cd748f3dc92235f22a7dfc42745644a96a", size = 5231472, upload-time = "2025-10-15T16:16:41.168Z" },
{ url = "https://files.pythonhosted.org/packages/3e/51/902b24fa8887e5fe2063fd61b1895a476d0bbf46811ab0c7fdf4bd127345/numpy-2.3.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:84f01a4d18b2cc4ade1814a08e5f3c907b079c847051d720fad15ce37aa930b6", size = 6739342, upload-time = "2025-10-15T16:16:43.777Z" },
{ url = "https://files.pythonhosted.org/packages/34/f1/4de9586d05b1962acdcdb1dc4af6646361a643f8c864cef7c852bf509740/numpy-2.3.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:817e719a868f0dacde4abdfc5c1910b301877970195db9ab6a5e2c4bd5b121f7", size = 14354338, upload-time = "2025-10-15T16:16:46.081Z" },
{ url = "https://files.pythonhosted.org/packages/1f/06/1c16103b425de7969d5a76bdf5ada0804b476fed05d5f9e17b777f1cbefd/numpy-2.3.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85e071da78d92a214212cacea81c6da557cab307f2c34b5f85b628e94803f9c0", size = 16702392, upload-time = "2025-10-15T16:16:48.455Z" },
{ url = "https://files.pythonhosted.org/packages/34/b2/65f4dc1b89b5322093572b6e55161bb42e3e0487067af73627f795cc9d47/numpy-2.3.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2ec646892819370cf3558f518797f16597b4e4669894a2ba712caccc9da53f1f", size = 16134998, upload-time = "2025-10-15T16:16:51.114Z" },
{ url = "https://files.pythonhosted.org/packages/d4/11/94ec578896cdb973aaf56425d6c7f2aff4186a5c00fac15ff2ec46998b46/numpy-2.3.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:035796aaaddfe2f9664b9a9372f089cfc88bd795a67bd1bfe15e6e770934cf64", size = 18651574, upload-time = "2025-10-15T16:16:53.429Z" },
{ url = "https://files.pythonhosted.org/packages/62/b7/7efa763ab33dbccf56dade36938a77345ce8e8192d6b39e470ca25ff3cd0/numpy-2.3.4-cp313-cp313t-win32.whl", hash = "sha256:fea80f4f4cf83b54c3a051f2f727870ee51e22f0248d3114b8e755d160b38cfb", size = 6413135, upload-time = "2025-10-15T16:16:55.992Z" },
{ url = "https://files.pythonhosted.org/packages/43/70/aba4c38e8400abcc2f345e13d972fb36c26409b3e644366db7649015f291/numpy-2.3.4-cp313-cp313t-win_amd64.whl", hash = "sha256:15eea9f306b98e0be91eb344a94c0e630689ef302e10c2ce5f7e11905c704f9c", size = 12928582, upload-time = "2025-10-15T16:16:57.943Z" },
{ url = "https://files.pythonhosted.org/packages/67/63/871fad5f0073fc00fbbdd7232962ea1ac40eeaae2bba66c76214f7954236/numpy-2.3.4-cp313-cp313t-win_arm64.whl", hash = "sha256:b6c231c9c2fadbae4011ca5e7e83e12dc4a5072f1a1d85a0a7b3ed754d145a40", size = 10266691, upload-time = "2025-10-15T16:17:00.048Z" },
{ url = "https://files.pythonhosted.org/packages/72/71/ae6170143c115732470ae3a2d01512870dd16e0953f8a6dc89525696069b/numpy-2.3.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:81c3e6d8c97295a7360d367f9f8553973651b76907988bb6066376bc2252f24e", size = 20955580, upload-time = "2025-10-15T16:17:02.509Z" },
{ url = "https://files.pythonhosted.org/packages/af/39/4be9222ffd6ca8a30eda033d5f753276a9c3426c397bb137d8e19dedd200/numpy-2.3.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7c26b0b2bf58009ed1f38a641f3db4be8d960a417ca96d14e5b06df1506d41ff", size = 14188056, upload-time = "2025-10-15T16:17:04.873Z" },
{ url = "https://files.pythonhosted.org/packages/6c/3d/d85f6700d0a4aa4f9491030e1021c2b2b7421b2b38d01acd16734a2bfdc7/numpy-2.3.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:62b2198c438058a20b6704351b35a1d7db881812d8512d67a69c9de1f18ca05f", size = 5116555, upload-time = "2025-10-15T16:17:07.499Z" },
{ url = "https://files.pythonhosted.org/packages/bf/04/82c1467d86f47eee8a19a464c92f90a9bb68ccf14a54c5224d7031241ffb/numpy-2.3.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:9d729d60f8d53a7361707f4b68a9663c968882dd4f09e0d58c044c8bf5faee7b", size = 6643581, upload-time = "2025-10-15T16:17:09.774Z" },
{ url = "https://files.pythonhosted.org/packages/0c/d3/c79841741b837e293f48bd7db89d0ac7a4f2503b382b78a790ef1dc778a5/numpy-2.3.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd0c630cf256b0a7fd9d0a11c9413b42fef5101219ce6ed5a09624f5a65392c7", size = 14299186, upload-time = "2025-10-15T16:17:11.937Z" },
{ url = "https://files.pythonhosted.org/packages/e8/7e/4a14a769741fbf237eec5a12a2cbc7a4c4e061852b6533bcb9e9a796c908/numpy-2.3.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5e081bc082825f8b139f9e9fe42942cb4054524598aaeb177ff476cc76d09d2", size = 16638601, upload-time = "2025-10-15T16:17:14.391Z" },
{ url = "https://files.pythonhosted.org/packages/93/87/1c1de269f002ff0a41173fe01dcc925f4ecff59264cd8f96cf3b60d12c9b/numpy-2.3.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:15fb27364ed84114438fff8aaf998c9e19adbeba08c0b75409f8c452a8692c52", size = 16074219, upload-time = "2025-10-15T16:17:17.058Z" },
{ url = "https://files.pythonhosted.org/packages/cd/28/18f72ee77408e40a76d691001ae599e712ca2a47ddd2c4f695b16c65f077/numpy-2.3.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:85d9fb2d8cd998c84d13a79a09cc0c1091648e848e4e6249b0ccd7f6b487fa26", size = 18576702, upload-time = "2025-10-15T16:17:19.379Z" },
{ url = "https://files.pythonhosted.org/packages/c3/76/95650169b465ececa8cf4b2e8f6df255d4bf662775e797ade2025cc51ae6/numpy-2.3.4-cp314-cp314-win32.whl", hash = "sha256:e73d63fd04e3a9d6bc187f5455d81abfad05660b212c8804bf3b407e984cd2bc", size = 6337136, upload-time = "2025-10-15T16:17:22.886Z" },
{ url = "https://files.pythonhosted.org/packages/dc/89/a231a5c43ede5d6f77ba4a91e915a87dea4aeea76560ba4d2bf185c683f0/numpy-2.3.4-cp314-cp314-win_amd64.whl", hash = "sha256:3da3491cee49cf16157e70f607c03a217ea6647b1cea4819c4f48e53d49139b9", size = 12920542, upload-time = "2025-10-15T16:17:24.783Z" },
{ url = "https://files.pythonhosted.org/packages/0d/0c/ae9434a888f717c5ed2ff2393b3f344f0ff6f1c793519fa0c540461dc530/numpy-2.3.4-cp314-cp314-win_arm64.whl", hash = "sha256:6d9cd732068e8288dbe2717177320723ccec4fb064123f0caf9bbd90ab5be868", size = 10480213, upload-time = "2025-10-15T16:17:26.935Z" },
{ url = "https://files.pythonhosted.org/packages/83/4b/c4a5f0841f92536f6b9592694a5b5f68c9ab37b775ff342649eadf9055d3/numpy-2.3.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:22758999b256b595cf0b1d102b133bb61866ba5ceecf15f759623b64c020c9ec", size = 21052280, upload-time = "2025-10-15T16:17:29.638Z" },
{ url = "https://files.pythonhosted.org/packages/3e/80/90308845fc93b984d2cc96d83e2324ce8ad1fd6efea81b324cba4b673854/numpy-2.3.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9cb177bc55b010b19798dc5497d540dea67fd13a8d9e882b2dae71de0cf09eb3", size = 14302930, upload-time = "2025-10-15T16:17:32.384Z" },
{ url = "https://files.pythonhosted.org/packages/3d/4e/07439f22f2a3b247cec4d63a713faae55e1141a36e77fb212881f7cda3fb/numpy-2.3.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:0f2bcc76f1e05e5ab58893407c63d90b2029908fa41f9f1cc51eecce936c3365", size = 5231504, upload-time = "2025-10-15T16:17:34.515Z" },
{ url = "https://files.pythonhosted.org/packages/ab/de/1e11f2547e2fe3d00482b19721855348b94ada8359aef5d40dd57bfae9df/numpy-2.3.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:8dc20bde86802df2ed8397a08d793da0ad7a5fd4ea3ac85d757bf5dd4ad7c252", size = 6739405, upload-time = "2025-10-15T16:17:36.128Z" },
{ url = "https://files.pythonhosted.org/packages/3b/40/8cd57393a26cebe2e923005db5134a946c62fa56a1087dc7c478f3e30837/numpy-2.3.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e199c087e2aa71c8f9ce1cb7a8e10677dc12457e7cc1be4798632da37c3e86e", size = 14354866, upload-time = "2025-10-15T16:17:38.884Z" },
{ url = "https://files.pythonhosted.org/packages/93/39/5b3510f023f96874ee6fea2e40dfa99313a00bf3ab779f3c92978f34aace/numpy-2.3.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85597b2d25ddf655495e2363fe044b0ae999b75bc4d630dc0d886484b03a5eb0", size = 16703296, upload-time = "2025-10-15T16:17:41.564Z" },
{ url = "https://files.pythonhosted.org/packages/41/0d/19bb163617c8045209c1996c4e427bccbc4bbff1e2c711f39203c8ddbb4a/numpy-2.3.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:04a69abe45b49c5955923cf2c407843d1c85013b424ae8a560bba16c92fe44a0", size = 16136046, upload-time = "2025-10-15T16:17:43.901Z" },
{ url = "https://files.pythonhosted.org/packages/e2/c1/6dba12fdf68b02a21ac411c9df19afa66bed2540f467150ca64d246b463d/numpy-2.3.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e1708fac43ef8b419c975926ce1eaf793b0c13b7356cfab6ab0dc34c0a02ac0f", size = 18652691, upload-time = "2025-10-15T16:17:46.247Z" },
{ url = "https://files.pythonhosted.org/packages/f8/73/f85056701dbbbb910c51d846c58d29fd46b30eecd2b6ba760fc8b8a1641b/numpy-2.3.4-cp314-cp314t-win32.whl", hash = "sha256:863e3b5f4d9915aaf1b8ec79ae560ad21f0b8d5e3adc31e73126491bb86dee1d", size = 6485782, upload-time = "2025-10-15T16:17:48.872Z" },
{ url = "https://files.pythonhosted.org/packages/17/90/28fa6f9865181cb817c2471ee65678afa8a7e2a1fb16141473d5fa6bacc3/numpy-2.3.4-cp314-cp314t-win_amd64.whl", hash = "sha256:962064de37b9aef801d33bc579690f8bfe6c5e70e29b61783f60bcba838a14d6", size = 13113301, upload-time = "2025-10-15T16:17:50.938Z" },
{ url = "https://files.pythonhosted.org/packages/54/23/08c002201a8e7e1f9afba93b97deceb813252d9cfd0d3351caed123dcf97/numpy-2.3.4-cp314-cp314t-win_arm64.whl", hash = "sha256:8b5a9a39c45d852b62693d9b3f3e0fe052541f804296ff401a72a1b60edafb29", size = 10547532, upload-time = "2025-10-15T16:17:53.48Z" },
]
[[package]]
name = "openai"
version = "2.6.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "distro" },
{ name = "httpx" },
{ name = "jiter" },
{ name = "pydantic" },
{ name = "sniffio" },
{ name = "tqdm" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c4/44/303deb97be7c1c9b53118b52825cbd1557aeeff510f3a52566b1fa66f6a2/openai-2.6.1.tar.gz", hash = "sha256:27ae704d190615fca0c0fc2b796a38f8b5879645a3a52c9c453b23f97141bb49", size = 593043, upload-time = "2025-10-24T13:29:52.79Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/15/0e/331df43df633e6105ff9cf45e0ce57762bd126a45ac16b25a43f6738d8a2/openai-2.6.1-py3-none-any.whl", hash = "sha256:904e4b5254a8416746a2f05649594fa41b19d799843cd134dac86167e094edef", size = 1005551, upload-time = "2025-10-24T13:29:50.973Z" },
]
[[package]]
name = "openpyxl"
version = "3.1.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "et-xmlfile" },
]
sdist = { url = "https://files.pythonhosted.org/packages/3d/f9/88d94a75de065ea32619465d2f77b29a0469500e99012523b91cc4141cd1/openpyxl-3.1.5.tar.gz", hash = "sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050", size = 186464, upload-time = "2024-06-28T14:03:44.161Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c0/da/977ded879c29cbd04de313843e76868e6e13408a94ed6b987245dc7c8506/openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2", size = 250910, upload-time = "2024-06-28T14:03:41.161Z" },
]
[[package]]
name = "pandas"
version = "2.3.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "numpy" },
{ name = "python-dateutil" },
{ name = "pytz" },
{ name = "tzdata" },
]
sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cd/4b/18b035ee18f97c1040d94debd8f2e737000ad70ccc8f5513f4eefad75f4b/pandas-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56851a737e3470de7fa88e6131f41281ed440d29a9268dcbf0002da5ac366713", size = 11544671, upload-time = "2025-09-29T23:21:05.024Z" },
{ url = "https://files.pythonhosted.org/packages/31/94/72fac03573102779920099bcac1c3b05975c2cb5f01eac609faf34bed1ca/pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8", size = 10680807, upload-time = "2025-09-29T23:21:15.979Z" },
{ url = "https://files.pythonhosted.org/packages/16/87/9472cf4a487d848476865321de18cc8c920b8cab98453ab79dbbc98db63a/pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32e7cc9af0f1cc15548288a51a3b681cc2a219faa838e995f7dc53dbab1062d", size = 11709872, upload-time = "2025-09-29T23:21:27.165Z" },
{ url = "https://files.pythonhosted.org/packages/15/07/284f757f63f8a8d69ed4472bfd85122bd086e637bf4ed09de572d575a693/pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac", size = 12306371, upload-time = "2025-09-29T23:21:40.532Z" },
{ url = "https://files.pythonhosted.org/packages/33/81/a3afc88fca4aa925804a27d2676d22dcd2031c2ebe08aabd0ae55b9ff282/pandas-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e0a175408804d566144e170d0476b15d78458795bb18f1304fb94160cabf40c", size = 12765333, upload-time = "2025-09-29T23:21:55.77Z" },
{ url = "https://files.pythonhosted.org/packages/8d/0f/b4d4ae743a83742f1153464cf1a8ecfafc3ac59722a0b5c8602310cb7158/pandas-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2d9ab0fc11822b5eece72ec9587e172f63cff87c00b062f6e37448ced4493", size = 13418120, upload-time = "2025-09-29T23:22:10.109Z" },
{ url = "https://files.pythonhosted.org/packages/4f/c7/e54682c96a895d0c808453269e0b5928a07a127a15704fedb643e9b0a4c8/pandas-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee", size = 10993991, upload-time = "2025-09-29T23:25:04.889Z" },
{ url = "https://files.pythonhosted.org/packages/f9/ca/3f8d4f49740799189e1395812f3bf23b5e8fc7c190827d55a610da72ce55/pandas-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:75ea25f9529fdec2d2e93a42c523962261e567d250b0013b16210e1d40d7c2e5", size = 12048227, upload-time = "2025-09-29T23:22:24.343Z" },
{ url = "https://files.pythonhosted.org/packages/0e/5a/f43efec3e8c0cc92c4663ccad372dbdff72b60bdb56b2749f04aa1d07d7e/pandas-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74ecdf1d301e812db96a465a525952f4dde225fdb6d8e5a521d47e1f42041e21", size = 11411056, upload-time = "2025-09-29T23:22:37.762Z" },
{ url = "https://files.pythonhosted.org/packages/46/b1/85331edfc591208c9d1a63a06baa67b21d332e63b7a591a5ba42a10bb507/pandas-2.3.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6435cb949cb34ec11cc9860246ccb2fdc9ecd742c12d3304989017d53f039a78", size = 11645189, upload-time = "2025-09-29T23:22:51.688Z" },
{ url = "https://files.pythonhosted.org/packages/44/23/78d645adc35d94d1ac4f2a3c4112ab6f5b8999f4898b8cdf01252f8df4a9/pandas-2.3.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:900f47d8f20860de523a1ac881c4c36d65efcb2eb850e6948140fa781736e110", size = 12121912, upload-time = "2025-09-29T23:23:05.042Z" },
{ url = "https://files.pythonhosted.org/packages/53/da/d10013df5e6aaef6b425aa0c32e1fc1f3e431e4bcabd420517dceadce354/pandas-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a45c765238e2ed7d7c608fc5bc4a6f88b642f2f01e70c0c23d2224dd21829d86", size = 12712160, upload-time = "2025-09-29T23:23:28.57Z" },
{ url = "https://files.pythonhosted.org/packages/bd/17/e756653095a083d8a37cbd816cb87148debcfcd920129b25f99dd8d04271/pandas-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc", size = 13199233, upload-time = "2025-09-29T23:24:24.876Z" },
{ url = "https://files.pythonhosted.org/packages/04/fd/74903979833db8390b73b3a8a7d30d146d710bd32703724dd9083950386f/pandas-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ee15f284898e7b246df8087fc82b87b01686f98ee67d85a17b7ab44143a3a9a0", size = 11540635, upload-time = "2025-09-29T23:25:52.486Z" },
{ url = "https://files.pythonhosted.org/packages/21/00/266d6b357ad5e6d3ad55093a7e8efc7dd245f5a842b584db9f30b0f0a287/pandas-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593", size = 10759079, upload-time = "2025-09-29T23:26:33.204Z" },
{ url = "https://files.pythonhosted.org/packages/ca/05/d01ef80a7a3a12b2f8bbf16daba1e17c98a2f039cbc8e2f77a2c5a63d382/pandas-2.3.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d2cefc361461662ac48810cb14365a365ce864afe85ef1f447ff5a1e99ea81c", size = 11814049, upload-time = "2025-09-29T23:27:15.384Z" },
{ url = "https://files.pythonhosted.org/packages/15/b2/0e62f78c0c5ba7e3d2c5945a82456f4fac76c480940f805e0b97fcbc2f65/pandas-2.3.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee67acbbf05014ea6c763beb097e03cd629961c8a632075eeb34247120abcb4b", size = 12332638, upload-time = "2025-09-29T23:27:51.625Z" },
{ url = "https://files.pythonhosted.org/packages/c5/33/dd70400631b62b9b29c3c93d2feee1d0964dc2bae2e5ad7a6c73a7f25325/pandas-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c46467899aaa4da076d5abc11084634e2d197e9460643dd455ac3db5856b24d6", size = 12886834, upload-time = "2025-09-29T23:28:21.289Z" },
{ url = "https://files.pythonhosted.org/packages/d3/18/b5d48f55821228d0d2692b34fd5034bb185e854bdb592e9c640f6290e012/pandas-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6253c72c6a1d990a410bc7de641d34053364ef8bcd3126f7e7450125887dffe3", size = 13409925, upload-time = "2025-09-29T23:28:58.261Z" },
{ url = "https://files.pythonhosted.org/packages/a6/3d/124ac75fcd0ecc09b8fdccb0246ef65e35b012030defb0e0eba2cbbbe948/pandas-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:1b07204a219b3b7350abaae088f451860223a52cfb8a6c53358e7948735158e5", size = 11109071, upload-time = "2025-09-29T23:32:27.484Z" },
{ url = "https://files.pythonhosted.org/packages/89/9c/0e21c895c38a157e0faa1fb64587a9226d6dd46452cac4532d80c3c4a244/pandas-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2462b1a365b6109d275250baaae7b760fd25c726aaca0054649286bcfbb3e8ec", size = 12048504, upload-time = "2025-09-29T23:29:31.47Z" },
{ url = "https://files.pythonhosted.org/packages/d7/82/b69a1c95df796858777b68fbe6a81d37443a33319761d7c652ce77797475/pandas-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7", size = 11410702, upload-time = "2025-09-29T23:29:54.591Z" },
{ url = "https://files.pythonhosted.org/packages/f9/88/702bde3ba0a94b8c73a0181e05144b10f13f29ebfc2150c3a79062a8195d/pandas-2.3.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a21d830e78df0a515db2b3d2f5570610f5e6bd2e27749770e8bb7b524b89b450", size = 11634535, upload-time = "2025-09-29T23:30:21.003Z" },
{ url = "https://files.pythonhosted.org/packages/a4/1e/1bac1a839d12e6a82ec6cb40cda2edde64a2013a66963293696bbf31fbbb/pandas-2.3.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e3ebdb170b5ef78f19bfb71b0dc5dc58775032361fa188e814959b74d726dd5", size = 12121582, upload-time = "2025-09-29T23:30:43.391Z" },
{ url = "https://files.pythonhosted.org/packages/44/91/483de934193e12a3b1d6ae7c8645d083ff88dec75f46e827562f1e4b4da6/pandas-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d051c0e065b94b7a3cea50eb1ec32e912cd96dba41647eb24104b6c6c14c5788", size = 12699963, upload-time = "2025-09-29T23:31:10.009Z" },
{ url = "https://files.pythonhosted.org/packages/70/44/5191d2e4026f86a2a109053e194d3ba7a31a2d10a9c2348368c63ed4e85a/pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87", size = 13202175, upload-time = "2025-09-29T23:31:59.173Z" },
]
[[package]]
name = "pydantic"
version = "2.12.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "annotated-types" },
{ name = "pydantic-core" },
{ name = "typing-extensions" },
{ name = "typing-inspection" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f3/1e/4f0a3233767010308f2fd6bd0814597e3f63f1dc98304a9112b8759df4ff/pydantic-2.12.3.tar.gz", hash = "sha256:1da1c82b0fc140bb0103bc1441ffe062154c8d38491189751ee00fd8ca65ce74", size = 819383, upload-time = "2025-10-17T15:04:21.222Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a1/6b/83661fa77dcefa195ad5f8cd9af3d1a7450fd57cc883ad04d65446ac2029/pydantic-2.12.3-py3-none-any.whl", hash = "sha256:6986454a854bc3bc6e5443e1369e06a3a456af9d339eda45510f517d9ea5c6bf", size = 462431, upload-time = "2025-10-17T15:04:19.346Z" },
]
[[package]]
name = "pydantic-core"
version = "2.41.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/df/18/d0944e8eaaa3efd0a91b0f1fc537d3be55ad35091b6a87638211ba691964/pydantic_core-2.41.4.tar.gz", hash = "sha256:70e47929a9d4a1905a67e4b687d5946026390568a8e952b92824118063cee4d5", size = 457557, upload-time = "2025-10-14T10:23:47.909Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/13/d0/c20adabd181a029a970738dfe23710b52a31f1258f591874fcdec7359845/pydantic_core-2.41.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:85e050ad9e5f6fe1004eec65c914332e52f429bc0ae12d6fa2092407a462c746", size = 2105688, upload-time = "2025-10-14T10:20:54.448Z" },
{ url = "https://files.pythonhosted.org/packages/00/b6/0ce5c03cec5ae94cca220dfecddc453c077d71363b98a4bbdb3c0b22c783/pydantic_core-2.41.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7393f1d64792763a48924ba31d1e44c2cfbc05e3b1c2c9abb4ceeadd912cced", size = 1910807, upload-time = "2025-10-14T10:20:56.115Z" },
{ url = "https://files.pythonhosted.org/packages/68/3e/800d3d02c8beb0b5c069c870cbb83799d085debf43499c897bb4b4aaff0d/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94dab0940b0d1fb28bcab847adf887c66a27a40291eedf0b473be58761c9799a", size = 1956669, upload-time = "2025-10-14T10:20:57.874Z" },
{ url = "https://files.pythonhosted.org/packages/60/a4/24271cc71a17f64589be49ab8bd0751f6a0a03046c690df60989f2f95c2c/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:de7c42f897e689ee6f9e93c4bec72b99ae3b32a2ade1c7e4798e690ff5246e02", size = 2051629, upload-time = "2025-10-14T10:21:00.006Z" },
{ url = "https://files.pythonhosted.org/packages/68/de/45af3ca2f175d91b96bfb62e1f2d2f1f9f3b14a734afe0bfeff079f78181/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:664b3199193262277b8b3cd1e754fb07f2c6023289c815a1e1e8fb415cb247b1", size = 2224049, upload-time = "2025-10-14T10:21:01.801Z" },
{ url = "https://files.pythonhosted.org/packages/af/8f/ae4e1ff84672bf869d0a77af24fd78387850e9497753c432875066b5d622/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d95b253b88f7d308b1c0b417c4624f44553ba4762816f94e6986819b9c273fb2", size = 2342409, upload-time = "2025-10-14T10:21:03.556Z" },
{ url = "https://files.pythonhosted.org/packages/18/62/273dd70b0026a085c7b74b000394e1ef95719ea579c76ea2f0cc8893736d/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1351f5bbdbbabc689727cb91649a00cb9ee7203e0a6e54e9f5ba9e22e384b84", size = 2069635, upload-time = "2025-10-14T10:21:05.385Z" },
{ url = "https://files.pythonhosted.org/packages/30/03/cf485fff699b4cdaea469bc481719d3e49f023241b4abb656f8d422189fc/pydantic_core-2.41.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1affa4798520b148d7182da0615d648e752de4ab1a9566b7471bc803d88a062d", size = 2194284, upload-time = "2025-10-14T10:21:07.122Z" },
{ url = "https://files.pythonhosted.org/packages/f9/7e/c8e713db32405dfd97211f2fc0a15d6bf8adb7640f3d18544c1f39526619/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7b74e18052fea4aa8dea2fb7dbc23d15439695da6cbe6cfc1b694af1115df09d", size = 2137566, upload-time = "2025-10-14T10:21:08.981Z" },
{ url = "https://files.pythonhosted.org/packages/04/f7/db71fd4cdccc8b75990f79ccafbbd66757e19f6d5ee724a6252414483fb4/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:285b643d75c0e30abda9dc1077395624f314a37e3c09ca402d4015ef5979f1a2", size = 2316809, upload-time = "2025-10-14T10:21:10.805Z" },
{ url = "https://files.pythonhosted.org/packages/76/63/a54973ddb945f1bca56742b48b144d85c9fc22f819ddeb9f861c249d5464/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:f52679ff4218d713b3b33f88c89ccbf3a5c2c12ba665fb80ccc4192b4608dbab", size = 2311119, upload-time = "2025-10-14T10:21:12.583Z" },
{ url = "https://files.pythonhosted.org/packages/f8/03/5d12891e93c19218af74843a27e32b94922195ded2386f7b55382f904d2f/pydantic_core-2.41.4-cp313-cp313-win32.whl", hash = "sha256:ecde6dedd6fff127c273c76821bb754d793be1024bc33314a120f83a3c69460c", size = 1981398, upload-time = "2025-10-14T10:21:14.584Z" },
{ url = "https://files.pythonhosted.org/packages/be/d8/fd0de71f39db91135b7a26996160de71c073d8635edfce8b3c3681be0d6d/pydantic_core-2.41.4-cp313-cp313-win_amd64.whl", hash = "sha256:d081a1f3800f05409ed868ebb2d74ac39dd0c1ff6c035b5162356d76030736d4", size = 2030735, upload-time = "2025-10-14T10:21:16.432Z" },
{ url = "https://files.pythonhosted.org/packages/72/86/c99921c1cf6650023c08bfab6fe2d7057a5142628ef7ccfa9921f2dda1d5/pydantic_core-2.41.4-cp313-cp313-win_arm64.whl", hash = "sha256:f8e49c9c364a7edcbe2a310f12733aad95b022495ef2a8d653f645e5d20c1564", size = 1973209, upload-time = "2025-10-14T10:21:18.213Z" },
{ url = "https://files.pythonhosted.org/packages/36/0d/b5706cacb70a8414396efdda3d72ae0542e050b591119e458e2490baf035/pydantic_core-2.41.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ed97fd56a561f5eb5706cebe94f1ad7c13b84d98312a05546f2ad036bafe87f4", size = 1877324, upload-time = "2025-10-14T10:21:20.363Z" },
{ url = "https://files.pythonhosted.org/packages/de/2d/cba1fa02cfdea72dfb3a9babb067c83b9dff0bbcb198368e000a6b756ea7/pydantic_core-2.41.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a870c307bf1ee91fc58a9a61338ff780d01bfae45922624816878dce784095d2", size = 1884515, upload-time = "2025-10-14T10:21:22.339Z" },
{ url = "https://files.pythonhosted.org/packages/07/ea/3df927c4384ed9b503c9cc2d076cf983b4f2adb0c754578dfb1245c51e46/pydantic_core-2.41.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d25e97bc1f5f8f7985bdc2335ef9e73843bb561eb1fa6831fdfc295c1c2061cf", size = 2042819, upload-time = "2025-10-14T10:21:26.683Z" },
{ url = "https://files.pythonhosted.org/packages/6a/ee/df8e871f07074250270a3b1b82aad4cd0026b588acd5d7d3eb2fcb1471a3/pydantic_core-2.41.4-cp313-cp313t-win_amd64.whl", hash = "sha256:d405d14bea042f166512add3091c1af40437c2e7f86988f3915fabd27b1e9cd2", size = 1995866, upload-time = "2025-10-14T10:21:28.951Z" },
{ url = "https://files.pythonhosted.org/packages/fc/de/b20f4ab954d6d399499c33ec4fafc46d9551e11dc1858fb7f5dca0748ceb/pydantic_core-2.41.4-cp313-cp313t-win_arm64.whl", hash = "sha256:19f3684868309db5263a11bace3c45d93f6f24afa2ffe75a647583df22a2ff89", size = 1970034, upload-time = "2025-10-14T10:21:30.869Z" },
{ url = "https://files.pythonhosted.org/packages/54/28/d3325da57d413b9819365546eb9a6e8b7cbd9373d9380efd5f74326143e6/pydantic_core-2.41.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:e9205d97ed08a82ebb9a307e92914bb30e18cdf6f6b12ca4bedadb1588a0bfe1", size = 2102022, upload-time = "2025-10-14T10:21:32.809Z" },
{ url = "https://files.pythonhosted.org/packages/9e/24/b58a1bc0d834bf1acc4361e61233ee217169a42efbdc15a60296e13ce438/pydantic_core-2.41.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:82df1f432b37d832709fbcc0e24394bba04a01b6ecf1ee87578145c19cde12ac", size = 1905495, upload-time = "2025-10-14T10:21:34.812Z" },
{ url = "https://files.pythonhosted.org/packages/fb/a4/71f759cc41b7043e8ecdaab81b985a9b6cad7cec077e0b92cff8b71ecf6b/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc3b4cc4539e055cfa39a3763c939f9d409eb40e85813257dcd761985a108554", size = 1956131, upload-time = "2025-10-14T10:21:36.924Z" },
{ url = "https://files.pythonhosted.org/packages/b0/64/1e79ac7aa51f1eec7c4cda8cbe456d5d09f05fdd68b32776d72168d54275/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b1eb1754fce47c63d2ff57fdb88c351a6c0150995890088b33767a10218eaa4e", size = 2052236, upload-time = "2025-10-14T10:21:38.927Z" },
{ url = "https://files.pythonhosted.org/packages/e9/e3/a3ffc363bd4287b80f1d43dc1c28ba64831f8dfc237d6fec8f2661138d48/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6ab5ab30ef325b443f379ddb575a34969c333004fca5a1daa0133a6ffaad616", size = 2223573, upload-time = "2025-10-14T10:21:41.574Z" },
{ url = "https://files.pythonhosted.org/packages/28/27/78814089b4d2e684a9088ede3790763c64693c3d1408ddc0a248bc789126/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:31a41030b1d9ca497634092b46481b937ff9397a86f9f51bd41c4767b6fc04af", size = 2342467, upload-time = "2025-10-14T10:21:44.018Z" },
{ url = "https://files.pythonhosted.org/packages/92/97/4de0e2a1159cb85ad737e03306717637842c88c7fd6d97973172fb183149/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a44ac1738591472c3d020f61c6df1e4015180d6262ebd39bf2aeb52571b60f12", size = 2063754, upload-time = "2025-10-14T10:21:46.466Z" },
{ url = "https://files.pythonhosted.org/packages/0f/50/8cb90ce4b9efcf7ae78130afeb99fd1c86125ccdf9906ef64b9d42f37c25/pydantic_core-2.41.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d72f2b5e6e82ab8f94ea7d0d42f83c487dc159c5240d8f83beae684472864e2d", size = 2196754, upload-time = "2025-10-14T10:21:48.486Z" },
{ url = "https://files.pythonhosted.org/packages/34/3b/ccdc77af9cd5082723574a1cc1bcae7a6acacc829d7c0a06201f7886a109/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c4d1e854aaf044487d31143f541f7aafe7b482ae72a022c664b2de2e466ed0ad", size = 2137115, upload-time = "2025-10-14T10:21:50.63Z" },
{ url = "https://files.pythonhosted.org/packages/ca/ba/e7c7a02651a8f7c52dc2cff2b64a30c313e3b57c7d93703cecea76c09b71/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b568af94267729d76e6ee5ececda4e283d07bbb28e8148bb17adad93d025d25a", size = 2317400, upload-time = "2025-10-14T10:21:52.959Z" },
{ url = "https://files.pythonhosted.org/packages/2c/ba/6c533a4ee8aec6b812c643c49bb3bd88d3f01e3cebe451bb85512d37f00f/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6d55fb8b1e8929b341cc313a81a26e0d48aa3b519c1dbaadec3a6a2b4fcad025", size = 2312070, upload-time = "2025-10-14T10:21:55.419Z" },
{ url = "https://files.pythonhosted.org/packages/22/ae/f10524fcc0ab8d7f96cf9a74c880243576fd3e72bd8ce4f81e43d22bcab7/pydantic_core-2.41.4-cp314-cp314-win32.whl", hash = "sha256:5b66584e549e2e32a1398df11da2e0a7eff45d5c2d9db9d5667c5e6ac764d77e", size = 1982277, upload-time = "2025-10-14T10:21:57.474Z" },
{ url = "https://files.pythonhosted.org/packages/b4/dc/e5aa27aea1ad4638f0c3fb41132f7eb583bd7420ee63204e2d4333a3bbf9/pydantic_core-2.41.4-cp314-cp314-win_amd64.whl", hash = "sha256:557a0aab88664cc552285316809cab897716a372afaf8efdbef756f8b890e894", size = 2024608, upload-time = "2025-10-14T10:21:59.557Z" },
{ url = "https://files.pythonhosted.org/packages/3e/61/51d89cc2612bd147198e120a13f150afbf0bcb4615cddb049ab10b81b79e/pydantic_core-2.41.4-cp314-cp314-win_arm64.whl", hash = "sha256:3f1ea6f48a045745d0d9f325989d8abd3f1eaf47dd00485912d1a3a63c623a8d", size = 1967614, upload-time = "2025-10-14T10:22:01.847Z" },
{ url = "https://files.pythonhosted.org/packages/0d/c2/472f2e31b95eff099961fa050c376ab7156a81da194f9edb9f710f68787b/pydantic_core-2.41.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6c1fe4c5404c448b13188dd8bd2ebc2bdd7e6727fa61ff481bcc2cca894018da", size = 1876904, upload-time = "2025-10-14T10:22:04.062Z" },
{ url = "https://files.pythonhosted.org/packages/4a/07/ea8eeb91173807ecdae4f4a5f4b150a520085b35454350fc219ba79e66a3/pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:523e7da4d43b113bf8e7b49fa4ec0c35bf4fe66b2230bfc5c13cc498f12c6c3e", size = 1882538, upload-time = "2025-10-14T10:22:06.39Z" },
{ url = "https://files.pythonhosted.org/packages/1e/29/b53a9ca6cd366bfc928823679c6a76c7a4c69f8201c0ba7903ad18ebae2f/pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5729225de81fb65b70fdb1907fcf08c75d498f4a6f15af005aabb1fdadc19dfa", size = 2041183, upload-time = "2025-10-14T10:22:08.812Z" },
{ url = "https://files.pythonhosted.org/packages/c7/3d/f8c1a371ceebcaf94d6dd2d77c6cf4b1c078e13a5837aee83f760b4f7cfd/pydantic_core-2.41.4-cp314-cp314t-win_amd64.whl", hash = "sha256:de2cfbb09e88f0f795fd90cf955858fc2c691df65b1f21f0aa00b99f3fbc661d", size = 1993542, upload-time = "2025-10-14T10:22:11.332Z" },
{ url = "https://files.pythonhosted.org/packages/8a/ac/9fc61b4f9d079482a290afe8d206b8f490e9fd32d4fc03ed4fc698214e01/pydantic_core-2.41.4-cp314-cp314t-win_arm64.whl", hash = "sha256:d34f950ae05a83e0ede899c595f312ca976023ea1db100cd5aa188f7005e3ab0", size = 1973897, upload-time = "2025-10-14T10:22:13.444Z" },
]
[[package]]
name = "python-dateutil"
version = "2.9.0.post0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "six" },
]
sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
]
[[package]]
name = "python-dotenv"
version = "1.2.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" },
]
[[package]]
name = "pytz"
version = "2025.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" },
]
[[package]]
name = "rapidfuzz"
version = "3.14.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ed/fc/a98b616db9a42dcdda7c78c76bdfdf6fe290ac4c5ffbb186f73ec981ad5b/rapidfuzz-3.14.1.tar.gz", hash = "sha256:b02850e7f7152bd1edff27e9d584505b84968cacedee7a734ec4050c655a803c", size = 57869570, upload-time = "2025-09-08T21:08:15.922Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0d/f2/0024cc8eead108c4c29337abe133d72ddf3406ce9bbfbcfc110414a7ea07/rapidfuzz-3.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8d69f470d63ee824132ecd80b1974e1d15dd9df5193916901d7860cef081a260", size = 1926515, upload-time = "2025-09-08T21:06:39.834Z" },
{ url = "https://files.pythonhosted.org/packages/12/ae/6cb211f8930bea20fa989b23f31ee7f92940caaf24e3e510d242a1b28de4/rapidfuzz-3.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6f571d20152fc4833b7b5e781b36d5e4f31f3b5a596a3d53cf66a1bd4436b4f4", size = 1388431, upload-time = "2025-09-08T21:06:41.73Z" },
{ url = "https://files.pythonhosted.org/packages/39/88/bfec24da0607c39e5841ced5594ea1b907d20f83adf0e3ee87fa454a425b/rapidfuzz-3.14.1-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:61d77e09b2b6bc38228f53b9ea7972a00722a14a6048be9a3672fb5cb08bad3a", size = 1375664, upload-time = "2025-09-08T21:06:43.737Z" },
{ url = "https://files.pythonhosted.org/packages/f4/43/9f282ba539e404bdd7052c7371d3aaaa1a9417979d2a1d8332670c7f385a/rapidfuzz-3.14.1-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8b41d95ef86a6295d353dc3bb6c80550665ba2c3bef3a9feab46074d12a9af8f", size = 1668113, upload-time = "2025-09-08T21:06:45.758Z" },
{ url = "https://files.pythonhosted.org/packages/7f/2f/0b3153053b1acca90969eb0867922ac8515b1a8a48706a3215c2db60e87c/rapidfuzz-3.14.1-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0591df2e856ad583644b40a2b99fb522f93543c65e64b771241dda6d1cfdc96b", size = 2212875, upload-time = "2025-09-08T21:06:47.447Z" },
{ url = "https://files.pythonhosted.org/packages/f8/9b/623001dddc518afaa08ed1fbbfc4005c8692b7a32b0f08b20c506f17a770/rapidfuzz-3.14.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f277801f55b2f3923ef2de51ab94689a0671a4524bf7b611de979f308a54cd6f", size = 3161181, upload-time = "2025-09-08T21:06:49.179Z" },
{ url = "https://files.pythonhosted.org/packages/ce/b7/d8404ed5ad56eb74463e5ebf0a14f0019d7eb0e65e0323f709fe72e0884c/rapidfuzz-3.14.1-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:893fdfd4f66ebb67f33da89eb1bd1674b7b30442fdee84db87f6cb9074bf0ce9", size = 1225495, upload-time = "2025-09-08T21:06:51.056Z" },
{ url = "https://files.pythonhosted.org/packages/2c/6c/b96af62bc7615d821e3f6b47563c265fd7379d7236dfbc1cbbcce8beb1d2/rapidfuzz-3.14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fe2651258c1f1afa9b66f44bf82f639d5f83034f9804877a1bbbae2120539ad1", size = 2396294, upload-time = "2025-09-08T21:06:53.063Z" },
{ url = "https://files.pythonhosted.org/packages/7f/b7/c60c9d22a7debed8b8b751f506a4cece5c22c0b05e47a819d6b47bc8c14e/rapidfuzz-3.14.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ace21f7a78519d8e889b1240489cd021c5355c496cb151b479b741a4c27f0a25", size = 2529629, upload-time = "2025-09-08T21:06:55.188Z" },
{ url = "https://files.pythonhosted.org/packages/25/94/a9ec7ccb28381f14de696ffd51c321974762f137679df986f5375d35264f/rapidfuzz-3.14.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:cb5acf24590bc5e57027283b015950d713f9e4d155fda5cfa71adef3b3a84502", size = 2782960, upload-time = "2025-09-08T21:06:57.339Z" },
{ url = "https://files.pythonhosted.org/packages/68/80/04e5276d223060eca45250dbf79ea39940c0be8b3083661d58d57572c2c5/rapidfuzz-3.14.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:67ea46fa8cc78174bad09d66b9a4b98d3068e85de677e3c71ed931a1de28171f", size = 3298427, upload-time = "2025-09-08T21:06:59.319Z" },
{ url = "https://files.pythonhosted.org/packages/4a/63/24759b2a751562630b244e68ccaaf7a7525c720588fcc77c964146355aee/rapidfuzz-3.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:44e741d785de57d1a7bae03599c1cbc7335d0b060a35e60c44c382566e22782e", size = 4267736, upload-time = "2025-09-08T21:07:01.31Z" },
{ url = "https://files.pythonhosted.org/packages/18/a4/73f1b1f7f44d55f40ffbffe85e529eb9d7e7f7b2ffc0931760eadd163995/rapidfuzz-3.14.1-cp313-cp313-win32.whl", hash = "sha256:b1fe6001baa9fa36bcb565e24e88830718f6c90896b91ceffcb48881e3adddbc", size = 1710515, upload-time = "2025-09-08T21:07:03.16Z" },
{ url = "https://files.pythonhosted.org/packages/6a/8b/a8fe5a6ee4d06fd413aaa9a7e0a23a8630c4b18501509d053646d18c2aa7/rapidfuzz-3.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:83b8cc6336709fa5db0579189bfd125df280a554af544b2dc1c7da9cdad7e44d", size = 1540081, upload-time = "2025-09-08T21:07:05.401Z" },
{ url = "https://files.pythonhosted.org/packages/ac/fe/4b0ac16c118a2367d85450b45251ee5362661e9118a1cef88aae1765ffff/rapidfuzz-3.14.1-cp313-cp313-win_arm64.whl", hash = "sha256:cf75769662eadf5f9bd24e865c19e5ca7718e879273dce4e7b3b5824c4da0eb4", size = 812725, upload-time = "2025-09-08T21:07:07.148Z" },
{ url = "https://files.pythonhosted.org/packages/e2/cb/1ad9a76d974d153783f8e0be8dbe60ec46488fac6e519db804e299e0da06/rapidfuzz-3.14.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d937dbeda71c921ef6537c6d41a84f1b8112f107589c9977059de57a1d726dd6", size = 1945173, upload-time = "2025-09-08T21:07:08.893Z" },
{ url = "https://files.pythonhosted.org/packages/d9/61/959ed7460941d8a81cbf6552b9c45564778a36cf5e5aa872558b30fc02b2/rapidfuzz-3.14.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7a2d80cc1a4fcc7e259ed4f505e70b36433a63fa251f1bb69ff279fe376c5efd", size = 1413949, upload-time = "2025-09-08T21:07:11.033Z" },
{ url = "https://files.pythonhosted.org/packages/7b/a0/f46fca44457ca1f25f23cc1f06867454fc3c3be118cd10b552b0ab3e58a2/rapidfuzz-3.14.1-cp313-cp313t-win32.whl", hash = "sha256:40875e0c06f1a388f1cab3885744f847b557e0b1642dfc31ff02039f9f0823ef", size = 1760666, upload-time = "2025-09-08T21:07:12.884Z" },
{ url = "https://files.pythonhosted.org/packages/9b/d0/7a5d9c04446f8b66882b0fae45b36a838cf4d31439b5d1ab48a9d17c8e57/rapidfuzz-3.14.1-cp313-cp313t-win_amd64.whl", hash = "sha256:876dc0c15552f3d704d7fb8d61bdffc872ff63bedf683568d6faad32e51bbce8", size = 1579760, upload-time = "2025-09-08T21:07:14.718Z" },
{ url = "https://files.pythonhosted.org/packages/4e/aa/2c03ae112320d0746f2c869cae68c413f3fe3b6403358556f2b747559723/rapidfuzz-3.14.1-cp313-cp313t-win_arm64.whl", hash = "sha256:61458e83b0b3e2abc3391d0953c47d6325e506ba44d6a25c869c4401b3bc222c", size = 832088, upload-time = "2025-09-08T21:07:17.03Z" },
{ url = "https://files.pythonhosted.org/packages/d6/36/53debca45fbe693bd6181fb05b6a2fd561c87669edb82ec0d7c1961a43f0/rapidfuzz-3.14.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e84d9a844dc2e4d5c4cabd14c096374ead006583304333c14a6fbde51f612a44", size = 1926336, upload-time = "2025-09-08T21:07:18.809Z" },
{ url = "https://files.pythonhosted.org/packages/ae/32/b874f48609665fcfeaf16cbaeb2bbc210deef2b88e996c51cfc36c3eb7c3/rapidfuzz-3.14.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:40301b93b99350edcd02dbb22e37ca5f2a75d0db822e9b3c522da451a93d6f27", size = 1389653, upload-time = "2025-09-08T21:07:20.667Z" },
{ url = "https://files.pythonhosted.org/packages/97/25/f6c5a1ff4ec11edadacb270e70b8415f51fa2f0d5730c2c552b81651fbe3/rapidfuzz-3.14.1-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fedd5097a44808dddf341466866e5c57a18a19a336565b4ff50aa8f09eb528f6", size = 1380911, upload-time = "2025-09-08T21:07:22.584Z" },
{ url = "https://files.pythonhosted.org/packages/d8/f3/d322202ef8fab463759b51ebfaa33228100510c82e6153bd7a922e150270/rapidfuzz-3.14.1-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e3e61c9e80d8c26709d8aa5c51fdd25139c81a4ab463895f8a567f8347b0548", size = 1673515, upload-time = "2025-09-08T21:07:24.417Z" },
{ url = "https://files.pythonhosted.org/packages/8d/b9/6b2a97f4c6be96cac3749f32301b8cdf751ce5617b1c8934c96586a0662b/rapidfuzz-3.14.1-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da011a373722fac6e64687297a1d17dc8461b82cb12c437845d5a5b161bc24b9", size = 2219394, upload-time = "2025-09-08T21:07:26.402Z" },
{ url = "https://files.pythonhosted.org/packages/11/bf/afb76adffe4406e6250f14ce48e60a7eb05d4624945bd3c044cfda575fbc/rapidfuzz-3.14.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5967d571243cfb9ad3710e6e628ab68c421a237b76e24a67ac22ee0ff12784d6", size = 3163582, upload-time = "2025-09-08T21:07:28.878Z" },
{ url = "https://files.pythonhosted.org/packages/42/34/e6405227560f61e956cb4c5de653b0f874751c5ada658d3532d6c1df328e/rapidfuzz-3.14.1-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:474f416cbb9099676de54aa41944c154ba8d25033ee460f87bb23e54af6d01c9", size = 1221116, upload-time = "2025-09-08T21:07:30.8Z" },
{ url = "https://files.pythonhosted.org/packages/55/e6/5b757e2e18de384b11d1daf59608453f0baf5d5d8d1c43e1a964af4dc19a/rapidfuzz-3.14.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ae2d57464b59297f727c4e201ea99ec7b13935f1f056c753e8103da3f2fc2404", size = 2402670, upload-time = "2025-09-08T21:07:32.702Z" },
{ url = "https://files.pythonhosted.org/packages/43/c4/d753a415fe54531aa882e288db5ed77daaa72e05c1a39e1cbac00d23024f/rapidfuzz-3.14.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:57047493a1f62f11354c7143c380b02f1b355c52733e6b03adb1cb0fe8fb8816", size = 2521659, upload-time = "2025-09-08T21:07:35.218Z" },
{ url = "https://files.pythonhosted.org/packages/cd/28/d4e7fe1515430db98f42deb794c7586a026d302fe70f0216b638d89cf10f/rapidfuzz-3.14.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:4acc20776f225ee37d69517a237c090b9fa7e0836a0b8bc58868e9168ba6ef6f", size = 2788552, upload-time = "2025-09-08T21:07:37.188Z" },
{ url = "https://files.pythonhosted.org/packages/4f/00/eab05473af7a2cafb4f3994bc6bf408126b8eec99a569aac6254ac757db4/rapidfuzz-3.14.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4373f914ff524ee0146919dea96a40a8200ab157e5a15e777a74a769f73d8a4a", size = 3306261, upload-time = "2025-09-08T21:07:39.624Z" },
{ url = "https://files.pythonhosted.org/packages/d1/31/2feb8dfcfcff6508230cd2ccfdde7a8bf988c6fda142fe9ce5d3eb15704d/rapidfuzz-3.14.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:37017b84953927807847016620d61251fe236bd4bcb25e27b6133d955bb9cafb", size = 4269522, upload-time = "2025-09-08T21:07:41.663Z" },
{ url = "https://files.pythonhosted.org/packages/a3/99/250538d73c8fbab60597c3d131a11ef2a634d38b44296ca11922794491ac/rapidfuzz-3.14.1-cp314-cp314-win32.whl", hash = "sha256:c8d1dd1146539e093b84d0805e8951475644af794ace81d957ca612e3eb31598", size = 1745018, upload-time = "2025-09-08T21:07:44.313Z" },
{ url = "https://files.pythonhosted.org/packages/c5/15/d50839d20ad0743aded25b08a98ffb872f4bfda4e310bac6c111fcf6ea1f/rapidfuzz-3.14.1-cp314-cp314-win_amd64.whl", hash = "sha256:f51c7571295ea97387bac4f048d73cecce51222be78ed808263b45c79c40a440", size = 1587666, upload-time = "2025-09-08T21:07:46.917Z" },
{ url = "https://files.pythonhosted.org/packages/a3/ff/d73fec989213fb6f0b6f15ee4bbdf2d88b0686197951a06b036111cd1c7d/rapidfuzz-3.14.1-cp314-cp314-win_arm64.whl", hash = "sha256:01eab10ec90912d7d28b3f08f6c91adbaf93458a53f849ff70776ecd70dd7a7a", size = 835780, upload-time = "2025-09-08T21:07:49.256Z" },
{ url = "https://files.pythonhosted.org/packages/b7/e7/f0a242687143cebd33a1fb165226b73bd9496d47c5acfad93de820a18fa8/rapidfuzz-3.14.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:60879fcae2f7618403c4c746a9a3eec89327d73148fb6e89a933b78442ff0669", size = 1945182, upload-time = "2025-09-08T21:07:51.84Z" },
{ url = "https://files.pythonhosted.org/packages/96/29/ca8a3f8525e3d0e7ab49cb927b5fb4a54855f794c9ecd0a0b60a6c96a05f/rapidfuzz-3.14.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f94d61e44db3fc95a74006a394257af90fa6e826c900a501d749979ff495d702", size = 1413946, upload-time = "2025-09-08T21:07:53.702Z" },
{ url = "https://files.pythonhosted.org/packages/b5/ef/6fd10aa028db19c05b4ac7fe77f5613e4719377f630c709d89d7a538eea2/rapidfuzz-3.14.1-cp314-cp314t-win32.whl", hash = "sha256:93b6294a3ffab32a9b5f9b5ca048fa0474998e7e8bb0f2d2b5e819c64cb71ec7", size = 1795851, upload-time = "2025-09-08T21:07:55.76Z" },
{ url = "https://files.pythonhosted.org/packages/e4/30/acd29ebd906a50f9e0f27d5f82a48cf5e8854637b21489bd81a2459985cf/rapidfuzz-3.14.1-cp314-cp314t-win_amd64.whl", hash = "sha256:6cb56b695421538fdbe2c0c85888b991d833b8637d2f2b41faa79cea7234c000", size = 1626748, upload-time = "2025-09-08T21:07:58.166Z" },
{ url = "https://files.pythonhosted.org/packages/c1/f4/dfc7b8c46b1044a47f7ca55deceb5965985cff3193906cb32913121e6652/rapidfuzz-3.14.1-cp314-cp314t-win_arm64.whl", hash = "sha256:7cd312c380d3ce9d35c3ec9726b75eee9da50e8a38e89e229a03db2262d3d96b", size = 853771, upload-time = "2025-09-08T21:08:00.816Z" },
]
[[package]]
name = "requests"
version = "2.32.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "charset-normalizer" },
{ name = "idna" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
]
[[package]]
name = "six"
version = "1.17.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
]
[[package]]
name = "sniffio"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
]
[[package]]
name = "starlette"
version = "0.49.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
]
sdist = { url = "https://files.pythonhosted.org/packages/1b/3f/507c21db33b66fb027a332f2cb3abbbe924cc3a79ced12f01ed8645955c9/starlette-0.49.1.tar.gz", hash = "sha256:481a43b71e24ed8c43b11ea02f5353d77840e01480881b8cb5a26b8cae64a8cb", size = 2654703, upload-time = "2025-10-28T17:34:10.928Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/51/da/545b75d420bb23b5d494b0517757b351963e974e79933f01e05c929f20a6/starlette-0.49.1-py3-none-any.whl", hash = "sha256:d92ce9f07e4a3caa3ac13a79523bd18e3bc0042bb8ff2d759a8e7dd0e1859875", size = 74175, upload-time = "2025-10-28T17:34:09.13Z" },
]
[[package]]
name = "tqdm"
version = "4.67.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" },
]
[[package]]
name = "typing-extensions"
version = "4.15.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
]
[[package]]
name = "typing-inspection"
version = "0.4.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
]
[[package]]
name = "tzdata"
version = "2025.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" },
]
[[package]]
name = "urllib3"
version = "2.5.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" },
]
[[package]]
name = "uvicorn"
version = "0.38.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "h11" },
]
sdist = { url = "https://files.pythonhosted.org/packages/cb/ce/f06b84e2697fef4688ca63bdb2fdf113ca0a3be33f94488f2cadb690b0cf/uvicorn-0.38.0.tar.gz", hash = "sha256:fd97093bdd120a2609fc0d3afe931d4d4ad688b6e75f0f929fde1bc36fe0e91d", size = 80605, upload-time = "2025-10-18T13:46:44.63Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ee/d9/d88e73ca598f4f6ff671fb5fde8a32925c2e08a637303a1d12883c7305fa/uvicorn-0.38.0-py3-none-any.whl", hash = "sha256:48c0afd214ceb59340075b4a052ea1ee91c16fbc2a9b1469cca0e54566977b02", size = 68109, upload-time = "2025-10-18T13:46:42.958Z" },
]

40
input-to-route/wrapper.py Normal file
View File

@@ -0,0 +1,40 @@
from database import search_database
from geocoder import validate_address
from new_parser_test import parse_request
from route import build_route
from ouput_chat import RouteDescriber
import json
if __name__=='__main__':
parsed = parse_request(input())
user_input, conversation_history = parsed['result'], parsed['conversation_history'],
#user_input = json.loads(user_input)
print(user_input)
query =user_input['tags']
user_address =user_input['user_location']
user_time =user_input['time']
val_output = validate_address('addresses.sqlite',user_address)
print(val_output)
found_points,mapping = search_database('output.json', query)
print(len(found_points))
print(mapping)
user_position =[]
if val_output['valid']:
user_position.append(val_output['coordinates']['lat'])
user_position.append(val_output['coordinates']['lon'])
else:
print('Адрес не найден')
n_nodes = len(mapping)
allow_extend = any(v[1] for v in mapping.values())
print(len(mapping),allow_extend)
route_otp =0
if allow_extend:
while n_nodes <= 5 and (route_otp := build_route(found_points, mapping, user_position, user_time, n_nodes,strategy='random') or route_otp): n_nodes += 1
else:
route_otp= build_route(found_points, mapping, user_position, user_time, n_nodes,strategy='random')
route, places = route_otp
dscb = RouteDescriber()
print(conversation_history)
description = dscb.generate_route_description(places,conversation_history)
print(route)
print(description)

View File

View File

@@ -0,0 +1,7 @@
FROM ghcr.io/project-osrm/osrm-backend:latest
WORKDIR /data
COPY data-final/ /data/
CMD ["osrm-routed", "--algorithm", "ch", "/data/nizhny.osrm"]

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

2
web-ui/.env Normal file
View File

@@ -0,0 +1,2 @@
VITE_API_BASE_URL=https://xczl-a4zl-n2ko.gw-1a.dockhost.net
VITE_OSRM_BASE_URL=https://8msn-80q0-el3y.gw-1a.dockhost.net

35
web-ui/.gitignore vendored Normal file
View File

@@ -0,0 +1,35 @@
# логи
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
# зависимости
node_modules/
# сборка
dist/
dist-ssr/
# локальные настройки
*.local
# папки редакторов
.vscode/*
!.vscode/extensions.json
.idea/
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# временные/отладочные файлы
*.tgz
npm-debug.log*
yarn-debug.log*
yarn-error.log*

3
web-ui/.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar"]
}

37
web-ui/Dockerfile Normal file
View File

@@ -0,0 +1,37 @@
# Stage 1: Build the Vue application
FROM node:20-alpine AS builder
# Set the working directory inside the container
WORKDIR /app
ARG VITE_API_BASE_URL
ARG VITE_OSRM_BASE_URL
# Make them available as environment variables during build
ENV VITE_API_BASE_URL=$VITE_API_BASE_URL
ENV VITE_OSRM_BASE_URL=$VITE_OSRM_BASE_URL
# Copy package.json and package-lock.json (if it exists)
# to leverage Docker cache for dependencies
COPY package.json package-lock.json* ./
# Install dependencies
RUN npm install
# Copy the rest of the application code
COPY . .
# Build the Vue application for production
RUN npm run build
# Stage 2: Serve the application with Nginx
FROM nginx:alpine
# Copy the built application from the builder stage to Nginx's default public directory
COPY --from=builder /app/dist /usr/share/nginx/html
# Expose port 80, which is the default for Nginx
EXPOSE 80
# Command to run Nginx in the foreground
CMD ["nginx", "-g", "daemon off;"]

5
web-ui/Makefile Normal file
View File

@@ -0,0 +1,5 @@
build_image:
docker build -t gitea.private.nikidze.ru/nikidze/gorkycode-web-ui .
push_image:
docker push gitea.private.nikidze.ru/nikidze/gorkycode-web-ui

5
web-ui/README.md Normal file
View File

@@ -0,0 +1,5 @@
# Vue 3 + Vite
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
Learn more about IDE Support for Vue in the [Vue Docs Scaling up Guide](https://vuejs.org/guide/scaling-up/tooling.html#ide-support).

13
web-ui/index.html Normal file
View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Gorkycode: Туризм</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

2473
web-ui/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

27
web-ui/package.json Normal file
View File

@@ -0,0 +1,27 @@
{
"name": "tourist-ai",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@tailwindcss/vite": "^4.1.16",
"axios": "^1.13.1",
"leaflet": "^1.9.4",
"lucide-vue-next": "^0.552.0",
"marked": "^16.4.1",
"pinia": "^3.0.3",
"vue": "^3.5.22"
},
"devDependencies": {
"@vitejs/plugin-vue": "^6.0.1",
"autoprefixer": "^10.4.21",
"postcss": "^8.5.6",
"tailwindcss": "^4.1.16",
"vite": "^7.1.7"
}
}

1
web-ui/public/vite.svg Normal file
View File

@@ -0,0 +1 @@
<svg fill="#000000" height="200px" width="200px" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 464.843 464.843" xml:space="preserve"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <g> <path d="M231.671,262.343c-15.991,0-29,13.01-29,29c0,15.99,13.009,29,29,29c15.991,0,29-13.01,29-29 C260.671,275.353,247.662,262.343,231.671,262.343z M231.671,312.343c-11.58,0-21-9.421-21-21s9.42-21,21-21 c11.58,0,21,9.421,21,21S243.251,312.343,231.671,312.343z"></path> <path d="M185.671,380.343c-2.209,0-4,1.791-4,4v12c0,2.209,1.791,4,4,4s4-1.791,4-4v-12 C189.671,382.134,187.88,380.343,185.671,380.343z"></path> <path d="M208.921,380.343c-2.209,0-4,1.791-4,4v12c0,2.209,1.791,4,4,4s4-1.791,4-4v-12 C212.921,382.134,211.13,380.343,208.921,380.343z"></path> <path d="M232.171,380.343c-2.209,0-4,1.791-4,4v12c0,2.209,1.791,4,4,4s4-1.791,4-4v-12 C236.171,382.134,234.38,380.343,232.171,380.343z"></path> <path d="M255.421,380.343c-2.209,0-4,1.791-4,4v12c0,2.209,1.791,4,4,4s4-1.791,4-4v-12 C259.421,382.134,257.63,380.343,255.421,380.343z"></path> <path d="M278.671,380.343c-2.209,0-4,1.791-4,4v12c0,2.209,1.791,4,4,4c2.209,0,4-1.791,4-4v-12 C282.671,382.134,280.88,380.343,278.671,380.343z"></path> <path d="M218.645,16.132l5.961,2.329l-3.454,5.388c-1.143,1.784-0.698,4.149,1.017,5.396c0.708,0.515,1.531,0.764,2.349,0.764 c1.16,0,2.308-0.503,3.096-1.464l4.057-4.95l4.057,4.95c0.799,0.974,1.978,1.453,3.143,1.464c2.196-0.016,3.971-1.801,3.971-4 c0-0.928-0.316-1.781-0.846-2.46l-3.261-5.087l5.961-2.329c1.974-0.77,3.003-2.947,2.349-4.962 c-0.655-2.015-2.765-3.162-4.817-2.634l-6.191,1.62l-0.373-6.39C235.541,1.652,233.79,0,231.671,0c-2.119,0-3.87,1.652-3.993,3.767 l-0.373,6.39l-6.191-1.62c-2.05-0.53-4.163,0.618-4.817,2.634C215.642,13.185,216.672,15.361,218.645,16.132z"></path> <path d="M397.987,360.278c-1.342-0.683-2.951-0.557-4.168,0.329l-10.688,7.773l-10.698-7.773c-1.217-0.884-2.826-1.012-4.167-0.328 c-1.34,0.684-2.184,2.061-2.184,3.565v26.5h-16.15v-26.5c0-1.505-0.844-2.882-2.185-3.565c-1.342-0.683-2.951-0.557-4.168,0.329 l-10.688,7.773l-10.688-7.773c-1.217-0.885-2.825-1.011-4.168-0.329c-1.34,0.683-2.184,2.06-2.184,3.565v26.5h-16.18v-59.5 c0-2.209-1.791-4-4-4h-2v-19.067c0-2.209-1.791-4-4-4c-2.209,0-4,1.791-4,4v19.067h-5.5v-75c0-2.209-1.791-4-4-4h-7v-91.5 c0-2.209-1.791-4-4-4h-7.959L235.602,37.851c-0.357-1.89-2.008-3.258-3.931-3.258c-1.923,0-3.574,1.368-3.931,3.258L206.13,152.343 h-7.959c-2.209,0-4,1.791-4,4v91.5h-7c-2.209,0-4,1.791-4,4v75h-6v-19.067c0-2.209-1.791-4-4-4s-4,1.791-4,4v19.067h-2 c-2.209,0-4,1.791-4,4v59.5h-14.18v-26.5c0-1.505-0.844-2.882-2.185-3.565c-1.341-0.683-2.951-0.557-4.168,0.329l-10.688,7.773 l-10.688-7.773c-1.216-0.885-2.826-1.011-4.168-0.329c-1.34,0.683-2.185,2.06-2.185,3.565v26.5h-16.15v-26.5 c0-1.504-0.844-2.881-2.184-3.565c-1.34-0.682-2.95-0.555-4.167,0.328l-10.697,7.773l-10.688-7.773 c-1.216-0.885-2.826-1.011-4.168-0.329c-1.34,0.683-2.185,2.06-2.185,3.565v97c0,2.209,1.791,4,4,4h327.5c2.209,0,4-1.791,4-4v-97 C400.171,362.338,399.327,360.961,397.987,360.278z M171.171,334.843h19.75v28.2c0,2.209,1.791,4,4,4s4-1.791,4-4v-28.2h28.5v28.2 c0,2.209,1.791,4,4,4s4-1.791,4-4v-28.2h28.5v28.2c0,2.209,1.791,4,4,4c2.209,0,4-1.791,4-4v-28.2h19.75v85h-120.5V334.843z M261.171,215.343h-11v-55h11V215.343z M242.171,160.343v55h-21v-55H242.171z M231.671,60.159l17.4,92.184h-34.8L231.671,60.159z M213.171,160.343v55h-11v-55H213.171z M202.171,223.343h59v24.5h-59V223.343z M191.171,255.843h81v71h-81V255.843z M72.671,371.697l6.688,4.863c1.402,1.02,3.302,1.018,4.704,0.001l6.699-4.868v22.649c0,2.209,1.791,4,4,4h24.15 c2.209,0,4-1.791,4-4v-22.646l6.688,4.863c1.402,1.02,3.303,1.02,4.705,0l6.688-4.863v22.646c0,2.209,1.791,4,4,4h18.18v58.5h-90.5 V371.697z M171.171,456.843v-29h120.5v29H171.171z M392.171,456.843h-92.5v-58.5h20.18c2.209,0,4-1.791,4-4v-22.646l6.688,4.863 c1.402,1.02,3.303,1.02,4.705,0l6.688-4.863v22.646c0,2.209,1.791,4,4,4h24.15c2.209,0,4-1.791,4-4v-22.649l6.699,4.868 c1.402,1.018,3.302,1.019,4.704-0.001l6.688-4.863V456.843z"></path> <path d="M241.671,287.343h-6v-6.125c0-2.209-1.791-4-4-4s-4,1.791-4,4v10.125c0,2.209,1.791,4,4,4h10c2.209,0,4-1.791,4-4 C245.671,289.134,243.88,287.343,241.671,287.343z"></path> </g> </g></svg>

After

Width:  |  Height:  |  Size: 4.2 KiB

81
web-ui/src/App.vue Normal file
View File

@@ -0,0 +1,81 @@
<template>
<div class="w-full max-w-md mx-auto h-screen overflow-hidden flex flex-col bg-white">
<!-- Верхняя панель -->
<div v-if="store.screen != 1 && store.screen != 6" class="flex justify-between items-center p-1 border-b border-gray-200">
<!-- Кнопка Назад -->
<button
@click="goBack"
class="rounded-full p-2 hover:bg-gray-100 transition disabled:opacity-40 disabled:cursor-not-allowed"
:disabled="store.screen === 1"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-700" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
</svg>
</button>
<!-- Кнопка Домой -->
<button
@click="goHome"
class="rounded-full p-2 hover:bg-gray-100 transition"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-700" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0h6" />
</svg>
</button>
</div>
<!-- Контейнер экранов -->
<div class="flex-1 relative overflow-hidden">
<Transition
enter-from-class="transform translate-y-full"
enter-active-class="transition-transform duration-500 ease-out"
enter-to-class="transform translate-y-0"
leave-from-class="transform translate-y-0"
leave-active-class="transition-transform duration-500 ease-out"
leave-to-class="transform -translate-y-full"
>
<component
:is="currentScreen"
:key="store.screen"
class="absolute top-0 left-0 w-full h-full"
/>
</Transition>
</div>
</div>
</template>
<script setup>
import { computed } from "vue";
import { useMainStore } from "./store/main";
import ScreenWelcome from "./components/ScreenWelcome.vue";
import ScreenAddress from "./components/ScreenAddress.vue";
import ScreenInterests from "./components/ScreenInterests.vue";
import ScreenDuration from "./components/ScreenDuration.vue";
import ScreenChat from "./components/ScreenChat.vue";
import ScreenRoute from "./components/ScreenRoute.vue";
const store = useMainStore();
const currentScreen = computed(() => {
return [
ScreenWelcome,
ScreenAddress,
ScreenInterests,
ScreenDuration,
ScreenChat,
ScreenRoute,
][store.screen - 1];
});
// Навигация
function goBack() {
if (store.screen > 1) {
store.screen--;
}
}
function goHome() {
store.screen = 1;
}
</script>

View File

@@ -0,0 +1,5 @@
@import 'tailwindcss';
:root {
--btn: #06b6d4;
}

View File

@@ -0,0 +1,44 @@
<template>
<div class="flex flex-col h-full bg-white">
<div class="p-5 border-b border-gray-100">
<h2 class="text-xl font-semibold">Где ты находишься?</h2>
<p class="text-sm text-gray-500 mt-1">
Укажи адрес в свободной форме. Для точного поиска рекомендую ввести в формате: улица Такая-то, дом N.
Приложение работает по Нижнему Новгороду.
</p>
</div>
<div class="flex-1 p-5">
<input
v-model="store.address"
class="w-full border rounded-lg p-3 focus:outline-none focus:ring-2 focus:ring-cyan-500"
placeholder="улица Дальняя, дом 8"
/>
</div>
<div class="p-4 border-t border-gray-100">
<button
:disabled="!store.address.trim()"
:class="{
'opacity-50 cursor-not-allowed': !store.address.trim()
}"
class="w-full bg-cyan-500 hover:bg-cyan-600 text-white py-3 rounded-2xl font-semibold transition"
@click="goNext"
>
Продолжить
</button>
</div>
</div>
</template>
<script setup>
import { useMainStore } from "../store/main";
const store = useMainStore();
function goNext() {
if (store.address.trim()) {
store.screen = 3;
}
}
</script>

View File

@@ -0,0 +1,181 @@
<template>
<div class="flex flex-col h-full bg-white">
<div class="p-5 border-b border-gray-100">
<h2 class="text-xl font-semibold">
Уточняем детали маршрута
</h2>
</div>
<div class="flex-1 overflow-y-auto p-5" ref="messagesContainer">
<div
v-for="(msg, index) in messages"
:key="index"
:class="[
'mb-4 p-3 rounded-lg max-w-[80%]',
msg.role === 'user'
? 'ml-auto bg-cyan-500 text-white'
: 'mr-auto bg-gray-100 text-gray-800'
]"
>
<p class="text-sm whitespace-pre-wrap">{{ msg.content }}</p>
</div>
<div v-if="loading" class="mb-4 p-3 rounded-lg max-w-[80%] mr-auto bg-gray-100 text-gray-800">
<p class="text-sm">Думаю...</p>
</div>
<div ref="bottomRef"></div>
</div>
<div class="p-4 border-t border-gray-100">
<div class="flex gap-2">
<input
v-model="userInput"
@keyup.enter="sendMessage"
:disabled="loading || routeBuilt"
class="flex-1 border rounded-lg p-3 focus:outline-none focus:ring-2 focus:ring-cyan-500 disabled:bg-gray-100"
placeholder="Напишите сообщение..."
/>
<button
@click="sendMessage"
:disabled="!userInput.trim() || loading || routeBuilt"
:class="{
'opacity-50 cursor-not-allowed': !userInput.trim() || loading || routeBuilt
}"
class="bg-cyan-500 hover:bg-cyan-600 text-white px-6 rounded-lg font-semibold transition"
>
Отправить
</button>
</div>
</div>
</div>
</template>
<script setup>
import {ref, onMounted, nextTick} from 'vue';
import {useMainStore} from '../store/main';
import axios from 'axios';
const store = useMainStore();
const messages = ref([]);
const conversationHistory = ref([]);
const userInput = ref('');
const loading = ref(false);
const routeBuilt = ref(false);
const messagesContainer = ref(null);
const bottomRef = ref(null);
onMounted(async () => {
// Отправляем первое сообщение
const initialMessage = `Я нахожусь: ${store.address}. Мне интересно: ${store.interests}. У меня есть время: ${store.duration}`;
messages.value.push({
role: 'user',
content: initialMessage
});
await scrollToBottom();
await sendToServer(initialMessage);
});
async function sendMessage() {
if (!userInput.value.trim() || loading.value || routeBuilt.value) return;
const message = userInput.value.trim();
userInput.value = '';
messages.value.push({
role: 'user',
content: message
});
await scrollToBottom();
await sendToServer(message);
}
async function sendToServer(userMessage) {
loading.value = true;
try {
const response = await axios.post(import.meta.env.VITE_API_BASE_URL+'/chat', {
user_input: userMessage,
conversation_history: conversationHistory.value
}, {
timeout: 60000 // 60 секунд
});
const data = response.data;
// Обновляем историю разговора
conversationHistory.value = data.conversation_history || [];
if (data.success && !data.need_more_info) {
messages.value.push({
role: 'assistant',
content: data.result.description
});
store.route = {
coordinates: data.result.route,
places: data.result.places,
description: data.result.description,
};
routeBuilt.value = true;
await scrollToBottom();
setTimeout(() => {
store.screen = 6;
}, 2000);
} else if (data.need_more_info) {
const lastMessage = data.conversation_history[data.conversation_history.length - 1];
const assistantMessage = lastMessage?.content || 'Пожалуйста, уточните детали.';
messages.value.push({
role: 'assistant',
content: assistantMessage
});
await scrollToBottom();
} else if (data.error) {
messages.value.push({
role: 'assistant',
content: data.error
});
await scrollToBottom();
}
} catch (error) {
console.error('Chat error:', error);
// Проверка на таймаут
if (error.code === 'ECONNABORTED') {
messages.value.push({
role: 'assistant',
content: 'Время ожидания истекло. Попробуйте еще раз.'
});
} else {
messages.value.push({
role: 'assistant',
content: 'Произошла ошибка. Попробуйте еще раз.'
});
}
await scrollToBottom();
} finally {
loading.value = false;
}
}
async function scrollToBottom() {
await nextTick();
if (bottomRef.value) {
bottomRef.value.scrollIntoView({behavior: 'smooth'});
}
}
</script>

View File

@@ -0,0 +1,45 @@
<template>
<div class="flex flex-col h-full bg-white">
<div class="p-5 border-b border-gray-100">
<h2 class="text-xl font-semibold">
Сколько времени у тебя на прогулку?
</h2>
<p class="text-sm text-gray-500 mt-1">
Укажи время в любом формате: «1 час», «90 минут», «полтора часа», «до 6 вечера» и т.д.
</p>
</div>
<div class="flex-1 p-5">
<input
v-model="store.duration"
class="w-full border rounded-lg p-3 focus:outline-none focus:ring-2 focus:ring-cyan-500"
placeholder="2 часа"
/>
</div>
<div class="p-4 border-t border-gray-100">
<button
:disabled="!store.duration || !store.duration.trim()"
:class="{
'opacity-50 cursor-not-allowed': !store.duration || !store.duration.trim()
}"
class="w-full bg-cyan-500 hover:bg-cyan-600 text-white py-3 rounded-2xl font-semibold transition"
@click="goNext"
>
Продолжить
</button>
</div>
</div>
</template>
<script setup>
import {useMainStore} from "../store/main";
const store = useMainStore();
function goNext() {
if (store.duration && store.duration.trim()) {
store.screen = 5;
}
}
</script>

View File

@@ -0,0 +1,58 @@
<template>
<div class="flex flex-col h-full bg-white">
<div class="p-5">
<h2 class="text-xl font-semibold">Как ты представляешь свой маршрут?</h2>
<p class="text-sm text-gray-500 mt-1">
Опиши в свободной форме.
</p>
</div>
<div class="flex-1 p-5 overflow-y-auto">
<textarea
v-model="store.interests"
@input="resizeTextarea"
ref="textareaRef"
class="w-full border rounded-lg p-3 focus:outline-none focus:ring-2 focus:ring-cyan-500"
rows="3"
placeholder="Я хочу культурно провести время, а потом выпить кофе"
></textarea>
</div>
<div class="p-4">
<button
class="w-full bg-cyan-500 text-white py-3 rounded-2xl font-semibold cursor-pointer"
:class="{
'hover:bg-cyan-600': isButtonEnabled,
'opacity-50 cursor-not-allowed': !isButtonEnabled,
}"
@click="store.screen = 4"
:disabled="!isButtonEnabled"
>
Далее
</button>
</div>
</div>
</template>
<script setup>
import { computed, ref, nextTick } from "vue";
import { useMainStore } from "../store/main";
const store = useMainStore();
const textareaRef = ref(null);
const isButtonEnabled = computed(() => {
return store.interests.trim().length > 0;
});
function resizeTextarea() {
if (textareaRef.value) {
textareaRef.value.style.height = "auto";
textareaRef.value.style.height = textareaRef.value.scrollHeight + "px";
}
}
nextTick(() => {
resizeTextarea();
});
</script>

View File

@@ -0,0 +1,286 @@
<template>
<div class="flex flex-col h-full bg-white">
<!-- Шапка -->
<div class="p-4 flex justify-between items-center border-b">
<h2 class="text-lg font-semibold">Ваш маршрут готов</h2>
<button
class="text-sm text-cyan-600 cursor-pointer hover:underline"
@click="restart"
>
Начать заново
</button>
</div>
<!-- Основной контент с прокруткой -->
<div class="flex-1 overflow-y-auto p-4 space-y-6">
<!-- Контейнер карты -->
<div class="relative w-full h-64 rounded-lg overflow-hidden border">
<div id="map" class="absolute inset-0"></div>
</div>
<!-- Описание маршрута -->
<div v-if="store.route?.description" class="prose prose-sm max-w-none">
<div v-html="renderedDescription" class="text-gray-700 leading-relaxed"></div>
</div>
<!-- Список точек маршрута -->
<div>
<h3 class="font-semibold text-lg mb-3">Точки маршрута</h3>
<div class="space-y-5">
<div v-for="(place, index) in store.route?.places" :key="index" class="border-b pb-4 last:border-b-0">
<div class="flex items-start gap-3">
<div class="flex-shrink-0 bg-cyan-500 text-white rounded-full h-8 w-8 flex items-center justify-center font-bold">
{{ index + 1 }}
</div>
<div class="flex-grow">
<h4 class="font-semibold text-md">{{ place.title || place.name }}</h4>
<div class="text-xs text-gray-500 mt-1 flex items-center gap-4 flex-wrap">
<span v-if="place.time_to_travel" class="flex items-center gap-1">
<WalkIcon class="w-4 h-4" /> До точки: ~{{ Math.round(place.time_to_travel) }} мин.
</span>
<span v-if="place.time_to_visit" class="flex items-center gap-1">
<HourglassIcon class="w-4 h-4" /> На точке: {{ place.time_to_visit }} мин.
</span>
</div>
<p v-if="place.explanation" class="text-sm text-gray-700 mt-2">
{{ stripQuotes(place.explanation) }}
</p>
<p v-if="place.description" class="text-sm text-gray-700 mt-2">
{{ place.description }}
</p>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="p-4 border-t">
<button
class="w-full border py-3 rounded-2xl cursor-pointer hover:bg-gray-50 transition"
@click="restart"
>
Построить ещё маршрут
</button>
</div>
</div>
</template>
<script setup>
import { onMounted, nextTick, computed } from "vue";
import { useMainStore } from "../store/main";
import { marked } from "marked";
import "leaflet/dist/leaflet.css";
import { Home as HomeIcon, Footprints as WalkIcon, Hourglass as HourglassIcon } from "lucide-vue-next";
const store = useMainStore();
let map;
const osrmBaseUrl = import.meta.env.VITE_OSRM_BASE_URL;
// Рендерим markdown в HTML
const renderedDescription = computed(() => {
if (!store.route?.description) return '';
return marked(store.route.description);
});
function stripQuotes(str) {
if (!str) return '';
return str.replace(/^"|"$/g, '');
}
async function getRouteFromOSRM() {
if (!osrmBaseUrl || !store.route?.places?.length) {
console.warn("OSRM URL не сконфигурирован или нет точек для маршрута.");
return null;
}
const allCoords = [
store.route.coordinates[0],
...store.route.places.map(p => {
if (typeof p.coordinate === 'string') {
return p.coordinate.split(',').map(Number);
} else if (Array.isArray(p.coordinate)) {
return p.coordinate;
}
return null;
}).filter(c => c !== null)
];
const coordsString = allCoords.map(coord => `${coord[1]},${coord[0]}`).join(';');
const url = `${osrmBaseUrl}/route/v1/foot/${coordsString}?overview=full&geometries=geojson`;
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`OSRM request failed with status: ${response.status}`);
}
const data = await response.json();
if (data.code === 'Ok' && data.routes && data.routes.length > 0) {
console.log("Маршрут от OSRM успешно получен.");
return data.routes[0].geometry;
} else {
throw new Error(`OSRM response error: ${data.message || 'No route found'}`);
}
} catch (error) {
console.error("Ошибка при получении маршрута от OSRM:", error);
return null;
}
}
async function initMap() {
if (!store.route?.coordinates) return;
const L = await import("leaflet");
await nextTick();
if (map) {
map.remove();
map = null;
}
const container = document.getElementById("map");
if (!container) return;
map = L.map(container, {
zoomControl: false,
attributionControl: false
}).setView(store.route.coordinates[0], 14);
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png").addTo(map);
let routeLayer;
const routeGeometry = await getRouteFromOSRM();
if (routeGeometry) {
routeLayer = L.geoJSON(routeGeometry, {
style: {
color: "#06b6d4",
weight: 5,
opacity: 0.8,
}
}).addTo(map);
} else {
console.warn("Не удалось построить маршрут через OSRM, рисуем прямые линии.");
routeLayer = L.polyline(store.route.coordinates, {
color: "#06b6d4",
weight: 5,
opacity: 0.8,
}).addTo(map);
}
store.route.places.forEach((place, i) => {
let lat, lon;
if (typeof place.coordinate === 'string') {
[lat, lon] = place.coordinate.split(',').map(Number);
} else if (Array.isArray(place.coordinate)) {
[lat, lon] = place.coordinate;
} else {
console.warn(`Неверный формат координат для точки ${i}`);
return;
}
const customIcon = L.divIcon({
className: 'custom-div-icon',
html: `<div style='background-color:#06b6d4; color:white; border-radius:50%; width:24px; height:24px; display:flex; align-items:center; justify-content:center; font-size:12px; font-weight:bold; border: 2px solid white;'>${i + 1}</div>`,
iconSize: [24, 24],
iconAnchor: [12, 12]
});
const popupContent = `<b>${place.title || place.name}</b>${place.address ? '<br>' + place.address : ''}`;
L.marker([lat, lon], {icon: customIcon})
.addTo(map)
.bindPopup(popupContent);
});
const startIcon = L.divIcon({
className: 'start-div-icon',
html: `
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24' style='color:#06b6d4;'>
<path
d='M3 9.75V21h6v-5.25a3 3 0 0 1 6 0V21h6V9.75L12 3 3 9.75z'
fill='#06b6d4'
stroke='white'
stroke-width='2'
stroke-linejoin='round'
/>
</svg>
`,
iconSize: [24, 24],
iconAnchor: [12, 24]
});
L.marker(store.route.coordinates[0], {icon: startIcon})
.addTo(map)
.bindPopup("<b>Вы здесь</b><br>" + store.address);
map.fitBounds(routeLayer.getBounds().pad(0.1));
setTimeout(() => map.invalidateSize(), 350);
}
onMounted(() => initMap());
function restart() {
store.reset();
}
</script>
<style scoped>
/* Стили для markdown контента */
.prose :deep(h1) {
font-size: 1.5rem;
font-weight: 700;
margin-top: 1rem;
margin-bottom: 0.5rem;
}
.prose :deep(h2) {
font-size: 1.25rem;
font-weight: 600;
margin-top: 0.75rem;
margin-bottom: 0.5rem;
}
.prose :deep(h3) {
font-size: 1.125rem;
font-weight: 600;
margin-top: 0.5rem;
margin-bottom: 0.5rem;
}
.prose :deep(p) {
margin-bottom: 0.75rem;
}
.prose :deep(ul), .prose :deep(ol) {
margin-left: 1.5rem;
margin-bottom: 0.75rem;
}
.prose :deep(li) {
margin-bottom: 0.25rem;
}
.prose :deep(strong) {
font-weight: 600;
}
.prose :deep(em) {
font-style: italic;
}
.prose :deep(code) {
background-color: #f3f4f6;
padding: 0.125rem 0.25rem;
border-radius: 0.25rem;
font-size: 0.875rem;
}
</style>

View File

@@ -0,0 +1,35 @@
<template>
<div class="h-full flex flex-col justify-between bg-[#0b61a8] text-white">
<div
class="flex-1 flex flex-col items-center justify-center text-center p-6"
>
<h1 class="text-2xl font-bold mb-2">
Привет. Я ИИ-помощник туриста.
</h1>
<p class="mt-6 mb-6">Давай составим пешеходный маршрут по Нижнему Новгороду?</p>
<div class="text-6xl mt-4 flex gap-4 justify-center">
<PlaneIcon class="w-12 h-12" />
<MapIcon class="w-12 h-12" />
</div>
<p class="mt-10">
Я помогу подобрать прогулку расскажи, где ты, что любишь и
сколько времени хочешь потратить.
</p>
</div>
<div class="p-4">
<button
class="w-full bg-cyan-500 hover:bg-cyan-600 text-white font-semibold py-3 rounded-2xl cursor-pointer"
@click="store.screen = 2"
>
Давай
</button>
</div>
</div>
</template>
<script setup>
import { useMainStore } from "../store/main";
import { Plane as PlaneIcon, Map as MapIcon } from "lucide-vue-next";
const store = useMainStore();
</script>

8
web-ui/src/main.js Normal file
View File

@@ -0,0 +1,8 @@
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import './assets/tailwind.css'
const app = createApp(App)
app.use(createPinia())
app.mount('#app')

26
web-ui/src/store/main.js Normal file
View File

@@ -0,0 +1,26 @@
import { defineStore } from 'pinia'
import { ref, reactive } from 'vue'
export const useMainStore = defineStore('main', () => {
const screen = ref(1)
const address = ref('')
const interests = ref('')
const duration = ref('')
const chat = reactive({ messages: [], asked: 0 })
function reset() {
screen.value = 1
address.value = ''
duration.value = ''
interests.value = ''
}
return {
screen,
address,
duration,
interests,
chat,
reset,
}
})

11
web-ui/tailwind.config.js Normal file
View File

@@ -0,0 +1,11 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{vue,js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}

10
web-ui/vite.config.js Normal file
View File

@@ -0,0 +1,10 @@
import {defineConfig} from 'vite'
import vue from '@vitejs/plugin-vue'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [
tailwindcss(),
vue(),
],
})