add regional converter

This commit is contained in:
2026-04-10 18:10:38 +04:00
parent dec08d5809
commit de3b5854c8
7 changed files with 628 additions and 181 deletions

View File

@@ -1,3 +1,3 @@
FROM nginx
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY ./interface.html /usr/share/nginx/html/index.html
COPY ./www /usr/share/nginx/html

38
k8s.yaml Normal file
View File

@@ -0,0 +1,38 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-converter-deployment
namespace: converter
labels:
app: nginx-converter
spec:
replicas: 1
selector:
matchLabels:
app: nginx-converter
template:
metadata:
labels:
app: nginx-converter
spec:
containers:
- name: nginx
image: git.danilkolesnikov.ru/danilko09/converter_nginx
ports:
- containerPort: 80
- name: swagger-ui
image: swaggerapi/swagger-ui
ports:
- containerPort: 8080
env:
- name: BASE_URL # <--- ADD THIS
value: "/docs"
- name: URLS
value: |
[
{ "url": "/app_openapi.json", "name": "Product matcher" },
{ "url": "/uc_openapi.json", "name": "Unit converter" },
{ "url": "/rc_openapi.json", "name": "Product matcher (region)" }
]
- name: URLS_PRIMARY_NAME
value: "Product matcher (region)"

View File

@@ -1,3 +1,9 @@
map $http_referer $openapi_backend {
"~*/uc/" http://10.100.10.70:9999/;
"~*/app/" http://10.100.11.188:8901/;
}
server {
listen 80;
server_name localhost;
@@ -8,10 +14,19 @@ server {
}
location /uc/ {
proxy_pass http://10.100.10.70:9999;
proxy_pass http://10.100.10.70:9999/;
}
location /app/ {
proxy_pass http://10.100.11.188:8901;
proxy_pass http://10.100.11.188:8903/;
}
location /region_search/ {
proxy_pass http://10.100.11.188:8902/;
}
location /docs/ {
proxy_pass http://localhost:8080;
proxy_set_header Host $host;
}
}

168
www/app_openapi.json Normal file
View File

@@ -0,0 +1,168 @@
{
"openapi": "3.1.0",
"info": { "title": "Product Matcher UI", "version": "1.0.0" },
"servers": [{ "url": "/app" }],
"paths": {
"/health": {
"get": {
"summary": "Health",
"operationId": "health_health_get",
"responses": {
"200": {
"description": "Successful Response",
"content": { "application/json": { "schema": {} } }
}
}
}
},
"/": {
"get": {
"summary": "Home",
"operationId": "home__get",
"responses": {
"200": {
"description": "Successful Response",
"content": { "text/html": { "schema": { "type": "string" } } }
}
}
}
},
"/search": {
"post": {
"summary": "Search Form",
"operationId": "search_form_search_post",
"requestBody": {
"content": {
"application/x-www-form-urlencoded": {
"schema": {
"$ref": "#/components/schemas/Body_search_form_search_post"
}
}
}
},
"responses": {
"200": {
"description": "Successful Response",
"content": { "text/html": { "schema": { "type": "string" } } }
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/HTTPValidationError" }
}
}
}
}
}
},
"/api/search": {
"post": {
"summary": "Search Api",
"operationId": "search_api_api_search_post",
"requestBody": {
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/SearchRequest" }
}
},
"required": true
},
"responses": {
"200": {
"description": "Successful Response",
"content": { "application/json": { "schema": {} } }
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/HTTPValidationError" }
}
}
}
}
}
}
},
"components": {
"schemas": {
"Body_search_form_search_post": {
"properties": {
"query_text": {
"type": "string",
"title": "Query Text",
"default": "песчано-гравийная смесь фракция 0-40"
},
"top_k": { "type": "integer", "title": "Top K", "default": 20 },
"use_rerank": {
"anyOf": [{ "type": "string" }, { "type": "null" }],
"title": "Use Rerank"
},
"rerank_strategy": {
"type": "string",
"title": "Rerank Strategy",
"default": "lexical"
}
},
"type": "object",
"title": "Body_search_form_search_post"
},
"HTTPValidationError": {
"properties": {
"detail": {
"items": { "$ref": "#/components/schemas/ValidationError" },
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
},
"SearchRequest": {
"properties": {
"query_text": {
"type": "string",
"title": "Query Text",
"default": "песчано-гравийная смесь фракция 0-40"
},
"top_k": {
"type": "integer",
"maximum": 1000.0,
"minimum": 1.0,
"title": "Top K",
"default": 20
},
"use_rerank": {
"type": "boolean",
"title": "Use Rerank",
"default": false
},
"rerank_strategy": {
"type": "string",
"title": "Rerank Strategy",
"default": "lexical"
}
},
"type": "object",
"title": "SearchRequest"
},
"ValidationError": {
"properties": {
"loc": {
"items": { "anyOf": [{ "type": "string" }, { "type": "integer" }] },
"type": "array",
"title": "Location"
},
"msg": { "type": "string", "title": "Message" },
"type": { "type": "string", "title": "Error Type" },
"input": { "title": "Input" },
"ctx": { "type": "object", "title": "Context" }
},
"type": "object",
"required": ["loc", "msg", "type"],
"title": "ValidationError"
}
}
}
}

View File

@@ -497,16 +497,10 @@
<div class="section-label">Параметры поиска</div>
<form class="form-card" id="searchForm" autocomplete="off">
<div class="grid-search">
<div class="field">
<label class="field-label" for="s_product">Наименование товара/услуги <span class="req">*</span></label>
<input type="text" id="s_product" placeholder="Введите наименование товара или услуги" required>
</div>
<div class="field">
<label class="field-label" for="s_count">Кол-во записей <span class="req">*</span></label>
<input type="number" id="s_count" placeholder="10" min="1" max="500" value="20">
</div>
</div>
<!-- Advanced -->
<div class="collapse-panel" id="s_advPanel" data-open="false">
@@ -570,14 +564,6 @@
<!-- Filters -->
<div class="filters-bar">
<div class="filters-grid">
<div class="field">
<label class="field-label">Дата закупки</label>
<div class="date-chips">
<button type="button" class="chip" data-days="7">Неделя</button>
<button type="button" class="chip" data-days="30">Месяц</button>
<button type="button" class="chip active" data-days="90">3 месяца</button>
</div>
</div>
<div class="field">
<label class="field-label" for="f_region">Регион</label>
<select id="f_region"><option value="">Все</option></select>
@@ -586,10 +572,6 @@
<label class="field-label" for="f_unit">Ед. измерения</label>
<select id="f_unit"><option value="">Все</option></select>
</div>
<div class="field">
<label class="field-label" for="f_qtyMin">Кол-во от</label>
<input type="number" id="f_qtyMin" placeholder="мин">
</div>
<div class="filters-actions">
<button type="button" class="btn btn-primary btn-sm" id="applyFilters">Применить</button>
<button type="button" class="btn btn-ghost btn-sm" id="resetFilters">Сбросить</button>
@@ -604,11 +586,11 @@
<thead>
<tr>
<th style="width:52px">#</th>
<th>Наименование товара/услуги</th>
<th>Наименование (raw)</th>
<th>Норм. наименование</th>
<th>Регион</th>
<th>Ед. изм.</th>
<th style="text-align:right">Кол-во</th>
<th>Дата закупки</th>
<th>ID контракта</th>
</tr>
</thead>
<tbody id="s_tbody"></tbody>
@@ -633,27 +615,17 @@
<input type="text" id="c_product" placeholder="Введите наименование товара или услуги" required>
</div>
<div class="grid-3">
<div class="field">
<label class="field-label" for="c_count">Кол-во записей</label>
<input type="number" id="c_count" placeholder="10" min="1" max="500" value="20">
</div>
<div class="field">
<label class="field-label" for="c_sourceUnit">Исходная ЕИ <span class="req">*</span></label>
<select id="c_sourceUnit" required>
<option value="">Выберите...</option>
<option>шт</option><option>кг</option><option>г</option>
<option>л</option><option>мл</option><option>м</option>
<option>см</option><option>упак</option><option>компл</option>
</select>
<input type="text" id="c_sourceUnit" placeholder="Например: кг" required>
</div>
<div class="field">
<label class="field-label" for="c_targetUnit">Целевая ЕИ <span class="req">*</span></label>
<select id="c_targetUnit" required>
<option value="">Выберите...</option>
<option>шт</option><option>кг</option><option>г</option>
<option>л</option><option>мл</option><option>м</option>
<option>см</option><option>упак</option><option>компл</option>
</select>
<input type="text" id="c_targetUnit" placeholder="Например: шт" required>
</div>
<div class="field">
<label class="field-label" for="c_attributes">Характеристики</label>
<input type="text" id="c_attributes" placeholder="Например: фракция 0-40, объём 500мл">
</div>
</div>
<div class="form-actions">
@@ -676,10 +648,8 @@
<p><strong>Укажите товар/услугу и единицы измерения</strong> — система рассчитает коэффициент пересчёта на основе данных из реестра закупок</p>
</div>
<!-- Converter result -->
<!-- Converter result (будет подключён когда появится API) -->
<div class="hidden" id="c_result">
<!-- Coefficient -->
<div class="result-card">
<div style="font-size:0.75rem;font-weight:600;letter-spacing:0.06em;text-transform:uppercase;color:var(--text-tertiary);margin-bottom:1rem">Коэффициент пересчёта</div>
<div class="coeff-display">
@@ -688,34 +658,6 @@
</div>
<div class="coeff-formula" id="c_formula"></div>
</div>
<!-- Logs -->
<div class="logs-section">
<button type="button" class="logs-toggle" id="logsToggle" aria-expanded="false">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M14 3v4a1 1 0 0 0 1 1h4"/>
<path d="M17 21H7a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h7l5 5v11a2 2 0 0 1-2 2z"/>
<path d="M9 17h6"/><path d="M9 13h6"/>
</svg>
<span>Логи бэкенда</span>
<span class="logs-badge" id="logsBadge">0</span>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<polyline points="6 9 12 15 18 9"/>
</svg>
</button>
<div class="collapse-panel" id="logsPanel" data-open="false">
<div class="collapse-inner">
<div class="logs-container">
<div class="logs-header">
<span>Backend logs</span>
<button class="btn btn-ghost btn-sm" style="color:var(--slate-400);font-size:0.75rem;padding:4px 8px" id="copyLogs">Копировать</button>
</div>
<div class="logs-body" id="logsBody"></div>
</div>
</div>
</div>
</div>
</div>
</div>
@@ -733,22 +675,7 @@ document.querySelectorAll('.tab-btn').forEach(btn => {
});
// ═══ TAB 1: Search ═══
const DEMO_DATA = [
{ name: 'Бумага офисная А4, 80 г/м², 500 листов', region: 'Москва', unit: 'упак', qty: 500, date: '2026-03-15' },
{ name: 'Бумага писчая А4 «Снегурочка»', region: 'Санкт-Петербург', unit: 'упак', qty: 200, date: '2026-03-10' },
{ name: 'Бумага для принтера А4, класс B', region: 'Новосибирская область', unit: 'упак', qty: 1000, date: '2026-02-28' },
{ name: 'Бумага офисная А3, 80 г/м²', region: 'Краснодарский край', unit: 'упак', qty: 150, date: '2026-02-20' },
{ name: 'Бумага для копировальных аппаратов А4', region: 'Москва', unit: 'шт', qty: 3000, date: '2026-02-14' },
{ name: 'Бумага SvetoCopy А4 500 л.', region: 'Свердловская область', unit: 'упак', qty: 800, date: '2026-01-30' },
{ name: 'Бумага мелованная А4 для лазерной печати', region: 'Республика Татарстан', unit: 'упак', qty: 50, date: '2026-01-22' },
{ name: 'Бумага офисная А4 «Ballet Classic»', region: 'Москва', unit: 'упак', qty: 2400, date: '2026-01-15' },
{ name: 'Бумага цветная А4, 10 цветов', region: 'Санкт-Петербург', unit: 'упак', qty: 120, date: '2026-01-08' },
{ name: 'Бумага для заметок 76×76 мм, жёлтая', region: 'Краснодарский край', unit: 'шт', qty: 5000, date: '2025-12-29' },
{ name: 'Бумага А4 для факсимильных аппаратов', region: 'Новосибирская область', unit: 'упак', qty: 300, date: '2025-12-20' },
{ name: 'Бумага крафт упаковочная 84 см', region: 'Москва', unit: 'м', qty: 1500, date: '2025-12-15' },
];
let allResults = [], filteredResults = [], activeDateFilter = 90;
let allResults = [], filteredResults = [];
const searchForm = document.getElementById('searchForm');
const searchBtn = document.getElementById('searchBtn');
@@ -767,13 +694,35 @@ document.getElementById('s_advToggle').addEventListener('click', function() {
});
// Submit search
searchForm.addEventListener('submit', (e) => {
searchForm.addEventListener('submit', async (e) => {
e.preventDefault();
const count = parseInt(document.getElementById('s_count').value) || 20;
allResults = DEMO_DATA.slice(0, Math.min(count, DEMO_DATA.length));
const queryText = s_product.value.trim();
const topK = 20;
const regions = [...new Set(allResults.map(r => r.region))];
const units = [...new Set(allResults.map(r => r.unit))];
searchBtn.disabled = true;
searchBtn.textContent = 'Поиск…';
try {
const resp = await fetch('/app/api/search', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query_text: queryText, top_k: topK, use_rerank: false, rerank_strategy: 'lexical' })
});
if (!resp.ok) throw new Error('Ошибка сервера: ' + resp.status);
const data = await resp.json();
allResults = data.results || [];
} catch (err) {
alert('Не удалось выполнить поиск: ' + err.message);
searchBtn.disabled = false;
searchBtn.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg> Найти похожие товары/услуги';
return;
}
searchBtn.disabled = false;
searchBtn.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg> Найти похожие товары/услуги';
const regions = [...new Set(allResults.map(r => r.region).filter(Boolean))];
const units = [...new Set(allResults.map(r => r.unit).filter(Boolean))];
document.getElementById('f_region').innerHTML = '<option value="">Все</option>' + regions.map(r => `<option>${r}</option>`).join('');
document.getElementById('f_unit').innerHTML = '<option value="">Все</option>' + units.map(u => `<option>${u}</option>`).join('');
@@ -784,26 +733,13 @@ searchForm.addEventListener('submit', (e) => {
requestAnimationFrame(() => { document.getElementById('s_results').style.animation = 'fadeUp 0.3s cubic-bezier(0.16,1,0.3,1) both'; });
});
// Date chips
document.querySelectorAll('.chip[data-days]').forEach(chip => {
chip.addEventListener('click', () => {
document.querySelectorAll('.chip').forEach(c => c.classList.remove('active'));
chip.classList.add('active');
activeDateFilter = parseInt(chip.dataset.days);
});
});
function applySearchFilters() {
const rVal = document.getElementById('f_region').value;
const uVal = document.getElementById('f_unit').value;
const qMin = parseFloat(document.getElementById('f_qtyMin').value) || 0;
const cutoff = new Date(); cutoff.setDate(cutoff.getDate() - activeDateFilter);
filteredResults = allResults.filter(r => {
if (rVal && r.region !== rVal) return false;
if (uVal && r.unit !== uVal) return false;
if (qMin && r.qty < qMin) return false;
if (new Date(r.date) < cutoff) return false;
return true;
});
renderSearchTable();
@@ -813,10 +749,6 @@ document.getElementById('applyFilters').addEventListener('click', applySearchFil
document.getElementById('resetFilters').addEventListener('click', () => {
document.getElementById('f_region').value = '';
document.getElementById('f_unit').value = '';
document.getElementById('f_qtyMin').value = '';
activeDateFilter = 90;
document.querySelectorAll('.chip').forEach(c => c.classList.remove('active'));
document.querySelector('.chip[data-days="90"]').classList.add('active');
applySearchFilters();
});
@@ -831,11 +763,11 @@ function renderSearchTable() {
tbody.innerHTML = filteredResults.map((r, i) => `
<tr>
<td class="cell-num">${i + 1}</td>
<td class="cell-name">${r.name}</td>
<td class="cell-region">${r.region}</td>
<td class="cell-unit">${r.unit}</td>
<td class="cell-qty">${r.qty.toLocaleString('ru-RU')}</td>
<td class="cell-date">${new Date(r.date).toLocaleDateString('ru-RU', {day:'2-digit',month:'2-digit',year:'numeric'})}</td>
<td class="cell-name">${r.raw_name || ''}</td>
<td class="cell-name">${r.norm_name || ''}</td>
<td class="cell-region">${r.region || '—'}</td>
<td class="cell-unit">${r.unit || ''}</td>
<td class="cell-num">${r.contract_id || ''}</td>
</tr>
`).join('');
}
@@ -848,75 +780,13 @@ const c_src = document.getElementById('c_sourceUnit');
const c_tgt = document.getElementById('c_targetUnit');
function validateConvert() {
convertBtn.disabled = !(c_product.value.trim() && c_src.value && c_tgt.value);
convertBtn.disabled = !(c_product.value.trim() && c_src.value.trim() && c_tgt.value.trim());
}
[c_product, c_src, c_tgt].forEach(el => el.addEventListener('input', validateConvert));
[c_src, c_tgt].forEach(el => el.addEventListener('change', validateConvert));
// Demo logs
function generateLogs(product, src, tgt) {
const now = new Date();
const t = (offset) => {
const d = new Date(now.getTime() - offset);
return d.toLocaleTimeString('ru-RU', {hour:'2-digit',minute:'2-digit',second:'2-digit'}) + '.' + String(d.getMilliseconds()).padStart(3,'0');
};
return [
{ time: t(2400), level: 'info', msg: `Получен запрос на конвертацию: "${product}"` },
{ time: t(2100), level: 'info', msg: `Параметры: source_unit=${src}, target_unit=${tgt}` },
{ time: t(1800), level: 'info', msg: 'Отправка запроса в Elasticsearch...' },
{ time: t(1200), level: 'ok', msg: `Elasticsearch: найдено 847 записей за 0.124s` },
{ time: t(1000), level: 'info', msg: 'Фильтрация записей с совпадающими ЕИ...' },
{ time: t(800), level: 'info', msg: `Отобрано 312 записей с ЕИ "${src}" и 89 с ЕИ "${tgt}"` },
{ time: t(600), level: 'info', msg: 'Расчёт медианного коэффициента пересчёта...' },
{ time: t(400), level: 'warn', msg: '3 выброса исключены из расчёта (отклонение > 3σ)' },
{ time: t(200), level: 'ok', msg: `Коэффициент рассчитан: 1 ${src} = 0.45 ${tgt}` },
{ time: t(50), level: 'ok', msg: 'Запрос выполнен успешно (2.35s)' },
];
}
convertForm.addEventListener('submit', (e) => {
e.preventDefault();
const product = c_product.value.trim();
const src = c_src.value;
const tgt = c_tgt.value;
// Show result
document.getElementById('c_empty').classList.add('hidden');
const resultEl = document.getElementById('c_result');
resultEl.classList.remove('hidden');
resultEl.style.animation = 'none';
requestAnimationFrame(() => { resultEl.style.animation = 'fadeUp 0.3s cubic-bezier(0.16,1,0.3,1) both'; });
// Demo coefficient
const coeff = 0.45;
document.getElementById('c_coeffValue').textContent = coeff;
document.getElementById('c_coeffLabel').innerHTML = `${src} → ${tgt}`;
document.getElementById('c_formula').textContent = `1 ${src} = ${coeff} ${tgt} • 1 ${tgt} = ${(1/coeff).toFixed(4)} ${src}`;
// Logs
const logs = generateLogs(product, src, tgt);
document.getElementById('logsBadge').textContent = logs.length;
document.getElementById('logsBody').innerHTML = logs.map(l =>
`<div class="log-line"><span class="log-time">${l.time}</span><span class="log-level ${l.level}">${l.level.toUpperCase().padEnd(4)}</span><span class="log-msg">${l.msg}</span></div>`
).join('');
});
// Logs toggle
document.getElementById('logsToggle').addEventListener('click', function() {
const panel = document.getElementById('logsPanel');
const open = panel.dataset.open === 'true';
panel.dataset.open = String(!open);
this.setAttribute('aria-expanded', String(!open));
});
// Copy logs
document.getElementById('copyLogs').addEventListener('click', () => {
const text = [...document.querySelectorAll('#logsBody .log-line')].map(l => l.textContent).join('\n');
navigator.clipboard.writeText(text).then(() => {
const btn = document.getElementById('copyLogs');
btn.textContent = 'Скопировано';
setTimeout(() => btn.textContent = 'Копировать', 1500);
});
// TODO: подключить API конвертера, когда будет готов endpoint
});
</script>

178
www/rc_openapi.json Normal file
View File

@@ -0,0 +1,178 @@
{
"openapi": "3.1.0",
"info": { "title": "Product Matcher Region UI", "version": "1.0.0" },
"servers": [{ "url": "/region_search" }],
"paths": {
"/health": {
"get": {
"summary": "Health",
"operationId": "health_health_get",
"responses": {
"200": {
"description": "Successful Response",
"content": { "application/json": { "schema": {} } }
}
}
}
},
"/": {
"get": {
"summary": "Home",
"operationId": "home__get",
"responses": {
"200": {
"description": "Successful Response",
"content": { "text/html": { "schema": { "type": "string" } } }
}
}
}
},
"/search": {
"post": {
"summary": "Search Form",
"operationId": "search_form_search_post",
"requestBody": {
"content": {
"application/x-www-form-urlencoded": {
"schema": {
"$ref": "#/components/schemas/Body_search_form_search_post"
}
}
}
},
"responses": {
"200": {
"description": "Successful Response",
"content": { "text/html": { "schema": { "type": "string" } } }
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/HTTPValidationError" }
}
}
}
}
}
},
"/api/search": {
"post": {
"summary": "Search Api",
"operationId": "search_api_api_search_post",
"requestBody": {
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/SearchRegionRequest" }
}
},
"required": true
},
"responses": {
"200": {
"description": "Successful Response",
"content": { "application/json": { "schema": {} } }
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/HTTPValidationError" }
}
}
}
}
}
}
},
"components": {
"schemas": {
"Body_search_form_search_post": {
"properties": {
"query_text": {
"type": "string",
"title": "Query Text",
"default": "песчано-гравийная смесь фракция 0-40"
},
"region": {
"type": "string",
"title": "Region",
"default": "москва"
},
"top_k": { "type": "integer", "title": "Top K", "default": 20 },
"use_rerank": {
"anyOf": [{ "type": "string" }, { "type": "null" }],
"title": "Use Rerank"
},
"rerank_strategy": {
"type": "string",
"title": "Rerank Strategy",
"default": "lexical"
}
},
"type": "object",
"title": "Body_search_form_search_post"
},
"HTTPValidationError": {
"properties": {
"detail": {
"items": { "$ref": "#/components/schemas/ValidationError" },
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
},
"SearchRegionRequest": {
"properties": {
"query_text": {
"type": "string",
"title": "Query Text",
"default": "песчано-гравийная смесь фракция 0-40"
},
"region": {
"type": "string",
"title": "Region",
"default": "москва"
},
"top_k": {
"type": "integer",
"maximum": 1000.0,
"minimum": 1.0,
"title": "Top K",
"default": 20
},
"use_rerank": {
"type": "boolean",
"title": "Use Rerank",
"default": false
},
"rerank_strategy": {
"type": "string",
"title": "Rerank Strategy",
"default": "lexical"
}
},
"type": "object",
"title": "SearchRegionRequest"
},
"ValidationError": {
"properties": {
"loc": {
"items": { "anyOf": [{ "type": "string" }, { "type": "integer" }] },
"type": "array",
"title": "Location"
},
"msg": { "type": "string", "title": "Message" },
"type": { "type": "string", "title": "Error Type" },
"input": { "title": "Input" },
"ctx": { "type": "object", "title": "Context" }
},
"type": "object",
"required": ["loc", "msg", "type"],
"title": "ValidationError"
}
}
}
}

178
www/uc_openapi.json Normal file
View File

@@ -0,0 +1,178 @@
{
"openapi": "3.1.0",
"info": {
"title": "Конвертер ЕИ (simple_frontend)",
"description": "Обёртка над `unit_converter/pipeline.py`: приём параметров конвертации, запуск пайплайна, возврат логов. Интерфейс: статическая страница `/`.",
"version": "1.0.0"
},
"servers": [{ "url": "/uc" }],
"paths": {
"/api/units": {
"get": {
"tags": ["Справочники"],
"summary": "Подсказки по единицам измерения",
"operationId": "api_units_api_units_get",
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/UnitsResponse" }
}
}
}
}
}
},
"/api/convert": {
"post": {
"tags": ["Конвертация"],
"summary": "Запуск конвертации (pipeline.py)",
"operationId": "api_convert_api_convert_post",
"requestBody": {
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/ConvertRequest" }
}
},
"required": true
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/ConvertResponse" }
}
}
},
"400": { "description": "Не заполнены обязательные поля" },
"500": {
"description": "Ошибка запуска pipeline (например нет config/скрипта)"
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/HTTPValidationError" }
}
}
}
}
}
}
},
"components": {
"schemas": {
"ConvertRequest": {
"properties": {
"product_name": {
"type": "string",
"title": "Product Name",
"description": "Наименование товара или услуги"
},
"source_unit": {
"type": "string",
"title": "Source Unit",
"description": "Исходная единица измерения"
},
"target_unit": {
"type": "string",
"title": "Target Unit",
"description": "Целевая единица измерения"
},
"supplier_name": {
"type": "string",
"title": "Supplier Name",
"description": "Поставщик (необязательно)",
"default": ""
},
"product_characteristics": {
"type": "string",
"title": "Product Characteristics",
"description": "Текст характеристик (необязательно)",
"default": ""
},
"quantity": {
"type": "number",
"minimum": 0.0,
"title": "Quantity",
"description": "Количество в исходных ЕИ",
"default": 1.0
}
},
"type": "object",
"required": ["product_name", "source_unit", "target_unit"],
"title": "ConvertRequest"
},
"ConvertResponse": {
"properties": {
"process_ok": {
"type": "boolean",
"title": "Process Ok",
"description": "Процесс pipeline завершился с кодом 0"
},
"conversion_success": {
"type": "boolean",
"title": "Conversion Success",
"description": "True, если в выводе нет «перевод невозможен»"
},
"message": {
"type": "string",
"title": "Message",
"description": "Краткий итог для UI"
},
"pipeline_output": {
"type": "string",
"title": "Pipeline Output",
"description": "Полный stdout+stderr pipeline.py (логи)",
"default": ""
}
},
"type": "object",
"required": ["process_ok", "conversion_success", "message"],
"title": "ConvertResponse"
},
"HTTPValidationError": {
"properties": {
"detail": {
"items": { "$ref": "#/components/schemas/ValidationError" },
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
},
"UnitsResponse": {
"properties": {
"units": {
"items": { "type": "string" },
"type": "array",
"title": "Units",
"description": "Список наименований ЕИ из exist_units.csv"
}
},
"type": "object",
"required": ["units"],
"title": "UnitsResponse"
},
"ValidationError": {
"properties": {
"loc": {
"items": { "anyOf": [{ "type": "string" }, { "type": "integer" }] },
"type": "array",
"title": "Location"
},
"msg": { "type": "string", "title": "Message" },
"type": { "type": "string", "title": "Error Type" },
"input": { "title": "Input" },
"ctx": { "type": "object", "title": "Context" }
},
"type": "object",
"required": ["loc", "msg", "type"],
"title": "ValidationError"
}
}
}
}