Files
llm/scripts/management_console_server.py
T

1169 lines
48 KiB
Python

from __future__ import annotations
import argparse
import datetime as dt
import json
import mimetypes
import re
import subprocess
import sys
import threading
import uuid
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from urllib.parse import parse_qs, unquote, urlparse
from urllib.request import Request, urlopen
from build_1c_agent_intake import build_intake
from common import search_lexical_index
from normalize_1c_its_docs import TextExtractor, clean_its_text, decode_html
from route_1c_question import route_question
from resolve_1c_fact import member_area, resolve_from_route_index, resolve_from_snapshot, split_fact_path
ROOT = Path(__file__).resolve().parents[1]
DEFAULT_HOST = "127.0.0.1"
DEFAULT_PORT = 8770
STATIC_DIR = ROOT / "tools" / "management-console"
REPORTS = ROOT / "reports"
JOB_LOCK = threading.Lock()
JOBS: dict[str, dict] = {}
RAG_INDEX = ROOT / "plugins" / "1c" / "datasets" / "prepared" / "rag_index.json"
DEFAULT_1C_ROUTE_INDEX = ROOT / "reports" / "1c-sql" / "upo" / "unified-object-route-index.json"
OFFICIAL_DOCS = ROOT / "plugins" / "1c" / "rag" / "official-docs"
OFFICIAL_NORMALIZED_MANIFEST = OFFICIAL_DOCS / "normalized" / "manifest.json"
OFFICIAL_STATIC_DIR = OFFICIAL_DOCS / "static"
OFFICIAL_1C_SOURCE_TYPES = [
"official_1c_its_admin_guide",
"official_1c_its_admin_methodical_support",
"official_1c_its_bsp_doc",
"official_1c_its_client_server_admin_guide",
"official_1c_its_dev_section",
"official_1c_its_developer_book",
"official_1c_its_developer_guide",
"official_1c_its_developer_methodical_support",
"official_1c_its_development_standards",
"official_1c_its_fresh_configuration_guide",
"official_1c_its_fresh_developer_guide",
"official_1c_its_glossary",
"official_1c_its_integration_library_doc",
"official_1c_its_methodical_support",
"official_1c_its_platform_doc",
"official_1c_its_taxi_user_guide",
"official_1c_its_user_guide",
]
COMMANDS: dict[str, dict] = {
"artifact_manifest": {
"label": "Build artifact manifest",
"command": [sys.executable, "scripts/build_llm_artifact_manifest.py", "--output", "reports/llm-artifact-manifest.json"],
"timeout": 120,
"category": "artifacts",
},
"artifact_check": {
"label": "Check artifact manifest",
"command": [sys.executable, "scripts/check_llm_artifact_manifest.py", "--manifest", "reports/llm-artifact-manifest.json", "--output", "reports/llm-artifact-check.json"],
"timeout": 120,
"category": "artifacts",
},
"model_storage": {
"label": "Check model storage",
"command": [sys.executable, "scripts/check_model_storage.py", "--report", "reports/model-storage.json"],
"timeout": 120,
"category": "models",
},
"q6_download_start": {
"label": "Start Q6 1C model download",
"command": ["powershell", "-ExecutionPolicy", "Bypass", "-File", "scripts/download_1c_q6_model.ps1", "-Action", "start"],
"timeout": 30,
"category": "models",
},
"q6_download_status": {
"label": "Check Q6 1C model download",
"command": ["powershell", "-ExecutionPolicy", "Bypass", "-File", "scripts/download_1c_q6_model.ps1", "-Action", "status"],
"timeout": 30,
"category": "models",
},
"q6_download_stop": {
"label": "Stop Q6 1C model download",
"command": ["powershell", "-ExecutionPolicy", "Bypass", "-File", "scripts/download_1c_q6_model.ps1", "-Action", "stop"],
"timeout": 30,
"category": "models",
},
"q6_service_start": {
"label": "Start Q6 1C model service",
"command": ["powershell", "-ExecutionPolicy", "Bypass", "-File", "scripts/manage_gpu_q6_service.ps1", "-Action", "start"],
"timeout": 60,
"category": "models",
},
"q6_service_stop": {
"label": "Stop Q6 1C model service",
"command": ["powershell", "-ExecutionPolicy", "Bypass", "-File", "scripts/manage_gpu_q6_service.ps1", "-Action", "stop"],
"timeout": 60,
"category": "models",
},
"q6_service_restart": {
"label": "Restart Q6 1C model service",
"command": ["powershell", "-ExecutionPolicy", "Bypass", "-File", "scripts/manage_gpu_q6_service.ps1", "-Action", "restart"],
"timeout": 90,
"category": "models",
},
"q6_service_status": {
"label": "Check Q6 1C model service",
"command": ["powershell", "-ExecutionPolicy", "Bypass", "-File", "scripts/manage_gpu_q6_service.ps1", "-Action", "status"],
"timeout": 60,
"category": "models",
},
"1c_plugin": {
"label": "Check 1C plugin",
"command": [sys.executable, "scripts/check_1c_plugin.py", "--report", "reports/1c-plugin-health.json"],
"timeout": 180,
"category": "1c",
},
"1c_question_router": {
"label": "Check 1C question router",
"command": [sys.executable, "scripts/check_1c_question_router.py", "--output", "reports/1c-question-router.json", "--print"],
"timeout": 120,
"category": "1c",
},
"1c_agent_intake": {
"label": "Check 1C agent intake",
"command": [sys.executable, "scripts/check_1c_agent_intake.py", "--output", "reports/1c-agent-intake.json", "--print"],
"timeout": 120,
"category": "1c",
},
"1c_rag_freshness": {
"label": "Check 1C RAG freshness",
"command": [sys.executable, "scripts/check_1c_rag_freshness.py", "--print"],
"timeout": 60,
"category": "rag",
},
"1c_rag_source_governance": {
"label": "Check 1C RAG source governance",
"command": [sys.executable, "scripts/check_1c_rag_source_governance.py", "--output", "reports/1c-rag-source-governance.json", "--print"],
"timeout": 60,
"category": "rag",
},
"official_docs_private": {
"label": "Check official docs private artifacts",
"command": [sys.executable, "scripts/check_1c_official_docs_private_artifacts.py", "--output", "reports/1c-official-docs-private-check.json"],
"timeout": 60,
"category": "rag",
},
"official_docs_access": {
"label": "Check 1C:ITS protected access",
"command": ["powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", "$ErrorActionPreference='Stop'; Add-Type -AssemblyName System.Security; $p='plugins/1c/rag/official-docs/.local/its-cookie.dpapi.txt'; $b=[Convert]::FromBase64String((Get-Content -LiteralPath $p -Raw -Encoding UTF8).Trim()); $plain=[System.Security.Cryptography.ProtectedData]::Unprotect($b,$null,[System.Security.Cryptography.DataProtectionScope]::CurrentUser); $env:ONEC_ITS_COOKIE=[System.Text.Encoding]::UTF8.GetString($plain); python -X utf8 scripts/check_1c_its_access.py --print"],
"timeout": 120,
"category": "rag",
},
"official_docs_cookie_normalizer": {
"label": "Check 1C:ITS cookie normalizer",
"command": [sys.executable, "scripts/check_1c_its_cookie_normalizer.py", "--print"],
"timeout": 60,
"category": "rag",
},
"official_docs_quality": {
"label": "Check official docs quality",
"command": [sys.executable, "scripts/check_1c_official_docs_quality.py", "--output", "reports/1c-official-docs-quality.json", "--print"],
"timeout": 60,
"category": "rag",
},
"official_docs_ingestion_logic": {
"label": "Check official docs ingestion logic",
"command": [sys.executable, "scripts/check_1c_its_ingestion_logic.py"],
"timeout": 60,
"category": "rag",
},
"official_docs_fetch_plan": {
"label": "Inspect official docs fetch plan",
"command": [sys.executable, "scripts/inspect_1c_its_fetch_plan.py", "--output", "reports/1c-official-docs-fetch-plan.json", "--print"],
"timeout": 60,
"category": "rag",
},
"official_docs_discover_starts": {
"label": "Discover official docs start links",
"command": ["powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", "$ErrorActionPreference='Stop'; Add-Type -AssemblyName System.Security; $p='plugins/1c/rag/official-docs/.local/its-cookie.dpapi.txt'; $b=[Convert]::FromBase64String((Get-Content -LiteralPath $p -Raw -Encoding UTF8).Trim()); $plain=[System.Security.Cryptography.ProtectedData]::Unprotect($b,$null,[System.Security.Cryptography.DataProtectionScope]::CurrentUser); $env:ONEC_ITS_COOKIE=[System.Text.Encoding]::UTF8.GetString($plain); python -X utf8 scripts/discover_1c_its_start_links.py --output plugins/1c/rag/official-docs/start-links.json --print"],
"timeout": 120,
"category": "rag",
},
"official_docs_start_coverage": {
"label": "Check official docs start coverage",
"command": [sys.executable, "scripts/check_1c_its_start_link_coverage.py", "--output", "reports/1c-official-docs-start-coverage.json", "--print"],
"timeout": 60,
"category": "rag",
},
"official_docs_platform_versions": {
"label": "Build official docs platform version catalog",
"command": [sys.executable, "scripts/build_1c_its_platform_version_catalog.py", "--print"],
"timeout": 60,
"category": "rag",
},
"official_docs_download_media": {
"label": "Download official docs media",
"command": [sys.executable, "scripts/download_1c_its_media.py", "--output-manifest", "plugins/1c/rag/official-docs/media/manifest.json"],
"timeout": 600,
"category": "rag",
},
"official_docs_static_site": {
"label": "Build official docs static site",
"command": [sys.executable, "scripts/build_1c_its_static_site.py"],
"timeout": 180,
"category": "rag",
},
"official_docs_static_quality": {
"label": "Check official docs static site",
"command": [sys.executable, "scripts/check_1c_its_static_site.py", "--output", "reports/1c-official-docs-static-check.json", "--print"],
"timeout": 60,
"category": "rag",
},
"official_docs_reload_fresh": {
"label": "Reload official docs without resume",
"command": ["powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", "scripts/run_1c_its_docs_pipeline.ps1", "-MaxPages", "2000", "-NoResume"],
"timeout": 7200,
"category": "rag",
},
"powershell_scripts": {
"label": "Validate PowerShell scripts",
"command": [sys.executable, "scripts/check_powershell_scripts.py"],
"timeout": 120,
"category": "repo",
},
"its_cookie_prompt": {
"label": "Open 1C:ITS cookie dialog",
"command": ["powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", "scripts/set_1c_its_cookie.ps1"],
"timeout": 600,
"category": "settings",
},
}
def now_iso() -> str:
return dt.datetime.now(dt.UTC).isoformat()
def load_json(path: Path) -> dict:
if not path.exists():
return {}
try:
return json.loads(path.read_text(encoding="utf-8-sig"))
except (json.JSONDecodeError, OSError):
return {}
def file_info(path: Path) -> dict:
if not path.exists():
return {"path": str(path), "exists": False}
stat = path.stat()
return {
"path": str(path),
"exists": True,
"size_bytes": stat.st_size,
"mtime": dt.datetime.fromtimestamp(stat.st_mtime, dt.UTC).isoformat(),
}
def compact_artifact_manifest() -> dict:
manifest = load_json(REPORTS / "llm-artifact-manifest.json")
artifacts = []
for item in manifest.get("artifacts") or []:
artifacts.append(
{
"name": item.get("name"),
"exists": item.get("exists"),
"files": item.get("file_count"),
"size_bytes": item.get("total_size_bytes"),
"secret_like_files": len(item.get("secret_like_files") or []),
"path": item.get("path"),
}
)
return {
"created_at": manifest.get("created_at"),
"counts": manifest.get("counts") or {},
"artifacts": artifacts,
"file": file_info(REPORTS / "llm-artifact-manifest.json"),
}
def compact_model_storage() -> dict:
report = load_json(REPORTS / "model-storage.json")
q6_download = load_json(REPORTS / "qwen3-coder-q6-download-status.json")
models = []
for item in report.get("models") or []:
models.append(
{
"id": item.get("id"),
"type": item.get("type"),
"runtime": item.get("runtime"),
"quantization": item.get("quantization"),
"served_model_name": item.get("served_model_name"),
"filename": item.get("filename"),
"status": item.get("status"),
"reason": item.get("reason"),
"required": item.get("required"),
"storage_path": item.get("storage_path"),
}
)
return {
"status": report.get("status"),
"counts": report.get("counts") or {},
"models": models,
"q6_download": q6_download,
"q6_service": q6_service_status(),
"file": file_info(REPORTS / "model-storage.json"),
}
def q6_service_status() -> dict:
base_url = "http://docker-gpu.cin.su:8081"
expected_model = "qwen3-coder-1c-q6"
status = {
"state": "unknown",
"base_url": base_url,
"openai_base_url": f"{base_url}/v1",
"openai_api_key_hint": "dummy",
"expected_model": expected_model,
"models": [],
"available": False,
"container": "llm-llama-qwen3-coder-q6-test",
"container_status": "",
"error": "",
}
try:
result = subprocess.run(
[
"docker",
"--host",
"ssh://docker-gpu",
"ps",
"-a",
"--filter",
"name=^llm-llama-qwen3-coder-q6-test$",
"--format",
"{{.Status}}",
],
cwd=ROOT,
capture_output=True,
text=True,
timeout=8,
check=False,
)
status["container_status"] = result.stdout.strip()
except (OSError, subprocess.TimeoutExpired) as exc:
status["container_status"] = f"docker status error: {exc}"
try:
request = Request(f"{base_url}/v1/models", method="GET")
with urlopen(request, timeout=5) as response:
payload = json.loads(response.read().decode("utf-8"))
models = []
for item in payload.get("data") or payload.get("models") or []:
if isinstance(item, str):
models.append(item)
elif isinstance(item, dict):
models.append(str(item.get("id") or item.get("model") or item.get("name") or ""))
status["models"] = [model for model in models if model]
status["available"] = expected_model in status["models"]
status["state"] = "ok" if status["available"] else "warning"
except Exception as exc: # pragma: no cover - depends on live GPU host
status["state"] = "error"
status["error"] = str(exc)
return status
def compact_1c_health() -> dict:
report = load_json(REPORTS / "1c-plugin-health.json")
return {
"status": report.get("status"),
"failed_checks": report.get("failed_checks") or [],
"manifest": ((report.get("checks") or {}).get("manifest") or {}),
"file": file_info(REPORTS / "1c-plugin-health.json"),
}
def compact_rag() -> dict:
prepared = ROOT / "plugins" / "1c" / "datasets" / "prepared"
official = ROOT / "plugins" / "1c" / "rag" / "official-docs"
private_check = load_json(REPORTS / "1c-official-docs-private-check.json")
quality_check = load_json(REPORTS / "1c-official-docs-quality.json")
access_check = load_json(REPORTS / "1c-its-access-check.json")
static_check = load_json(REPORTS / "1c-official-docs-static-check.json")
start_coverage = load_json(REPORTS / "1c-official-docs-start-coverage.json")
platform_versions = load_json(official / "platform-versions.json")
fetch_progress = load_json(official / "raw" / "progress.json")
fetch_manifest = load_json(official / "raw" / "manifest.json")
return {
"corpus": file_info(prepared / "rag_corpus.jsonl"),
"index": file_info(prepared / "rag_index.json"),
"manifest": file_info(prepared / "rag_manifest.json"),
"official_raw": file_info(official / "raw" / "manifest.json"),
"official_start_links": file_info(official / "start-links.json"),
"official_platform_versions": {
"counts": platform_versions.get("counts") or {},
"defaults": platform_versions.get("defaults") or {},
"file": file_info(official / "platform-versions.json"),
},
"official_start_coverage": {
"passed": start_coverage.get("passed"),
"counts": start_coverage.get("counts") or {},
"by_category": start_coverage.get("by_category") or {},
"file": file_info(REPORTS / "1c-official-docs-start-coverage.json"),
},
"official_progress": {
"status": fetch_progress.get("status"),
"page_count": fetch_progress.get("page_count") or fetch_manifest.get("page_count"),
"error_count": fetch_progress.get("error_count") or fetch_manifest.get("error_count"),
"seen_count": fetch_progress.get("seen_count"),
"queue_size": fetch_progress.get("queue_size"),
"current_url": fetch_progress.get("current_url"),
"resumed_pages": fetch_progress.get("resumed_pages") or ((fetch_manifest.get("resume") or {}).get("resumable_pages")),
"skipped_existing": fetch_progress.get("skipped_existing") or ((fetch_manifest.get("resume") or {}).get("skipped_existing")),
"updated_at_unix": fetch_progress.get("updated_at_unix"),
"file": file_info(official / "raw" / "progress.json"),
},
"official_normalized": file_info(official / "normalized" / "manifest.json"),
"official_media": file_info(official / "media" / "manifest.json"),
"official_static": file_info(official / "static" / "manifest.json"),
"official_static_url": "/official-docs-static/index.html",
"official_static_check": {
"passed": static_check.get("passed"),
"counts": static_check.get("counts") or {},
"file": file_info(REPORTS / "1c-official-docs-static-check.json"),
},
"official_private_check": {
"passed": private_check.get("passed"),
"counts": private_check.get("counts") or {},
"file": file_info(REPORTS / "1c-official-docs-private-check.json"),
},
"official_access_check": {
"status": access_check.get("status"),
"findings": access_check.get("findings") or [],
"page": access_check.get("page") or {},
"src": access_check.get("src") or {},
"file": file_info(REPORTS / "1c-its-access-check.json"),
},
"official_quality": {
"passed": quality_check.get("passed"),
"counts": quality_check.get("counts") or {},
"file": file_info(REPORTS / "1c-official-docs-quality.json"),
},
}
def overview() -> dict:
with JOB_LOCK:
jobs = sorted(JOBS.values(), key=lambda item: item.get("created_at") or "", reverse=True)[:20]
return {
"schema": "management_console_overview.v1",
"generated_at": now_iso(),
"workspace": str(ROOT),
"commands": [
{"id": key, "label": value["label"], "category": value["category"]}
for key, value in COMMANDS.items()
],
"artifacts": compact_artifact_manifest(),
"models": compact_model_storage(),
"rag": compact_rag(),
"one_c": compact_1c_health(),
"reports": {
"artifact_manifest": file_info(REPORTS / "llm-artifact-manifest.json"),
"artifact_check": file_info(REPORTS / "llm-artifact-check.json"),
"model_chat_status": file_info(REPORTS / "model-chat" / "status.md"),
},
"jobs": jobs,
}
def primary_doc_text(doc: dict) -> str:
title = str(doc.get("title") or "").strip()
content = str(doc.get("content") or "").strip()
if not content:
return ""
metadata = doc.get("metadata") or {}
source_type = str(metadata.get("source_type") or "")
chunk_index = int(doc.get("chunk_index") or 0)
if source_type == "official_1c_its_glossary" and chunk_index > 0:
return ""
lines = content.splitlines()
heading_candidates = [title]
heading_candidates.extend(str(item) for item in (metadata.get("headings") or []) if item)
if source_type == "official_1c_its_glossary":
heading_candidates.sort(key=lambda value: (":: Глоссарий" not in value, value == title))
for candidate in heading_candidates:
if not candidate:
continue
exact_heading = f"# {candidate}"
heading_indexes = [index for index, line in enumerate(lines) if line.strip() == exact_heading]
if heading_indexes:
lines = lines[heading_indexes[0] + 1 :]
break
trimmed_lines = []
for line in lines:
if line.strip() == "Назад":
break
trimmed_lines.append(line)
clean_lines = []
for line in trimmed_lines:
normalized = line.strip()
if not normalized or normalized in {"-", title}:
continue
clean_lines.append(normalized)
result = "\n".join(clean_lines).strip() or title
glossary_noise = (
"Глоссарий разработчика",
"Информационная система 1С:ИТС",
"Мы используем файлы cookie",
"Результаты поиска",
)
if source_type == "official_1c_its_glossary" and any(marker in result for marker in glossary_noise):
return title
return result
def rag_snippet(doc: dict, max_chars: int = 1200) -> str:
primary = primary_doc_text(doc)
if primary:
return primary[:max_chars]
metadata = doc.get("metadata") or {}
if str(metadata.get("source_type") or "") == "official_1c_its_glossary":
return ""
return str(doc.get("content") or "").strip()[:max_chars]
def is_toc_like_text(text: str) -> bool:
lines = [line.strip() for line in text.splitlines() if line.strip()]
if not lines:
return False
list_like = 0
prose_like = 0
for line in lines:
plain = line.lstrip("-*# ").strip()
if not plain or plain in {"Документ", "Назад"}:
continue
if plain.startswith("Платформа 1С:Предприятие"):
continue
if re.match(r"^\d+(?:\.\d+)+\.\s+\S+", plain) or re.match(r"^Глава\s+\d+\.", plain):
list_like += 1
continue
if len(plain) >= 70 and not plain.endswith(":"):
prose_like += 1
return list_like >= 6 and prose_like <= max(2, list_like // 5)
def doc_content_kind(doc: dict) -> str:
metadata = doc.get("metadata") or {}
if str(metadata.get("access_blocked") or "").casefold() == "true":
return "access_blocked"
text = primary_doc_text(doc)
meaningful = [
line.strip().lstrip("-*# ").strip()
for line in text.splitlines()
if line.strip()
]
meaningful = [
line
for line in meaningful
if line and line not in {"Документ", "Назад"} and not line.startswith("Платформа 1С:Предприятие")
]
if not meaningful:
return "metadata_only"
if is_toc_like_text(text):
return "toc_like"
return "content"
def display_doc_title(doc: dict) -> str:
metadata = doc.get("metadata") or {}
source_type = str(metadata.get("source_type") or "")
headings = [str(item) for item in (metadata.get("headings") or []) if item]
if source_type == "official_1c_its_glossary":
for heading in headings:
if ":: Глоссарий" in heading:
return heading
return str(doc.get("title") or (headings[0] if headings else ""))
def rag_answer_from_results(question: str, results: list[dict]) -> str:
if not results:
return "Контекст по вопросу в RAG не найден."
content_kinds = [doc_content_kind(item["document"]) for item in results[:5]]
if content_kinds and all(kind in {"access_blocked", "toc_like", "metadata_only"} for kind in content_kinds):
if "access_blocked" in content_kinds:
return (
"Найдена страница официальной документации, но тело документа закрыто текущим cookie/access. "
"Обновите cookie ИТС и проверьте доступ; пока можно видеть только оболочку или оглавление."
)
return (
"Найдена страница-оглавление официальной документации, а не полный текст раздела. "
"Для уверенного ответа нужно загрузить конкретный подраздел из оглавления или получить тело документа через доступный hdoc/src-канал."
)
question_lower = question.casefold()
exact_titles = {
"форма": {"форма", "общая форма", "элемент формы"},
}
for keyword, titles in exact_titles.items():
if keyword in question_lower:
exact_matches = [
item
for item in results
if str(item["document"].get("title") or "").strip().casefold() in titles
]
if not exact_matches:
return (
"Найденный контекст не содержит точной страницы термина. "
"Ниже показаны ближайшие источники, но их недостаточно для уверенного ответа."
)
exact_text = "\n\n".join(primary_doc_text(item["document"])[:1400] for item in exact_matches[:2]).casefold()
definition_markers = ("предназнач", "объект", "содержит", "используется", "является", "отображ")
if any(marker in exact_text for marker in definition_markers):
return (
"Найдена точная страница термина в официальной документации. "
"Проверьте фрагменты источников ниже: краткий ответ пока строится извлечением."
)
return (
"Найдена точная страница термина в официальной документации, "
"но в загруженном фрагменте нет развернутого определения. Ниже показан источник."
)
top_text = "\n\n".join(primary_doc_text(item["document"])[:1400] for item in results[:3])
lowered = top_text.casefold()
if any(marker in lowered for marker in ("предназнач", "объект", "содержит", "используется", "является", "отображ")):
return "Контекст найден. Проверьте фрагменты источников ниже: краткий ответ пока строится извлечением."
return "Найденный контекст не содержит достаточного определения для уверенного ответа. Ниже показаны источники, которые были найдены."
def query_rag(payload: dict) -> dict:
question = str(payload.get("question") or "").strip()
if not question:
raise ValueError("question is required")
if "source_type" in payload:
source_type = str(payload.get("source_type") or "").strip()
else:
source_type = "official_1c_its_glossary"
if source_type in {"", "official_1c_docs"}:
source_types = OFFICIAL_1C_SOURCE_TYPES
source_scope = "official_1c_docs"
elif source_type == "all_with_examples":
source_types = None
source_scope = "all_with_examples"
else:
source_types = [source_type]
source_scope = source_type
limit = max(1, min(int(payload.get("limit") or 5), 12))
metadata_filters = {
"platform_version": str(payload.get("platform_version") or "").strip(),
"platform_doc_id": str(payload.get("platform_doc_id") or "").strip(),
}
index = load_json(RAG_INDEX)
if not index:
return {
"question": question,
"answer": "RAG index is missing or empty. Rebuild the corpus and index first.",
"results": [],
}
results = search_lexical_index(
index,
question,
limit=limit,
candidate_limit=max(limit * 5, 20),
dedupe_by_document=False,
min_score=0.0,
source_types=source_types,
metadata_filters=metadata_filters,
)
compact = []
for item in results:
doc = item["document"]
metadata = doc.get("metadata") or {}
snippet = rag_snippet(doc)
if not snippet:
continue
content_kind = doc_content_kind(doc)
compact.append(
{
"score": round(float(item.get("score") or 0), 4),
"source_path": doc.get("source_path"),
"title": display_doc_title(doc),
"chunk_index": doc.get("chunk_index"),
"source_type": metadata.get("source_type"),
"url": metadata.get("url"),
"platform_version": metadata.get("platform_version"),
"platform_doc_id": metadata.get("platform_doc_id"),
"doc_book": metadata.get("doc_book"),
"doc_coordinate": metadata.get("doc_coordinate"),
"access_blocked": metadata.get("access_blocked"),
"access_findings": metadata.get("access_findings"),
"content_kind": content_kind,
"snippet": snippet,
}
)
answer = rag_answer_from_results(question, [item for item in results if rag_snippet(item["document"])])
if source_scope in {"metadata", "all_with_examples"}:
answer = (
"Внимание: в выбранный контур входят примеры или снимки, они не подтверждают текущую базу. "
+ answer
)
return {
"question": question,
"source_type": source_type,
"source_scope": source_scope,
"metadata_filters": {key: value for key, value in metadata_filters.items() if value},
"answer": answer,
"result_count": len(compact),
"results": compact,
}
def resolve_workspace_path(value: str) -> Path:
if not value:
raise ValueError("source_path is required")
path = Path(value)
if not path.is_absolute():
path = ROOT / path
return path.resolve()
def query_1c_fact(payload: dict) -> dict:
source_kind = str(payload.get("source_kind") or "route_index").strip()
source_path = str(payload.get("source_path") or "").strip()
if not source_path and DEFAULT_1C_ROUTE_INDEX.exists() and source_kind == "route_index":
source_path = str(DEFAULT_1C_ROUTE_INDEX)
path = resolve_workspace_path(source_path)
if not path.is_file():
raise ValueError(f"source_path does not exist: {path}")
fact_path = str(payload.get("path") or "").strip()
if fact_path:
path_kind, path_name, path_section, path_member = split_fact_path(fact_path)
kind = str(payload.get("kind") or path_kind or "").strip() or None
name = str(payload.get("name") or path_name or "").strip()
table_section = str(payload.get("table_section") or path_section or "").strip() or None
member = str(payload.get("member") or path_member or "").strip() or None
else:
kind = str(payload.get("kind") or "").strip() or None
name = str(payload.get("name") or "").strip()
table_section = str(payload.get("table_section") or "").strip() or None
member = str(payload.get("member") or "").strip() or None
if not name:
raise ValueError("Use path or name")
common = {
"kind": kind,
"object_name": name,
"member": member,
"area": member_area(str(payload.get("area") or "any").strip() or "any"),
"table_section": table_section,
"view": str(payload.get("view") or "effective").strip() or "effective",
"extension": str(payload.get("extension") or "").strip() or None,
}
if source_kind == "route_index":
return resolve_from_route_index(load_json(path), index_path=path, **common)
if source_kind == "metadata_snapshot":
return resolve_from_snapshot(load_json(path), snapshot_path=path, **common)
raise ValueError(f"Unsupported source_kind: {source_kind}")
def query_1c_route(payload: dict) -> dict:
question = str(payload.get("question") or "").strip()
if not question:
raise ValueError("question is required")
source_path = str(payload.get("source_path") or "").strip()
index_path = resolve_workspace_path(source_path) if source_path else DEFAULT_1C_ROUTE_INDEX
view = str(payload.get("view") or "effective").strip() or "effective"
return route_question(question, index_path=index_path, view=view)
def query_1c_intake(payload: dict) -> dict:
question = str(payload.get("question") or "").strip()
if not question:
raise ValueError("question is required")
source_path = str(payload.get("source_path") or "").strip()
index_path = resolve_workspace_path(source_path) if source_path else DEFAULT_1C_ROUTE_INDEX
view = str(payload.get("view") or "effective").strip() or "effective"
return build_intake(question, index=index_path, view=view)
def official_doc_row(item: dict, status_name: str, index: int) -> dict:
quality = item.get("quality") or {}
return {
"index": index,
"status": status_name,
"source_id": item.get("source_id"),
"source_type": item.get("source_type"),
"title": item.get("title"),
"url": item.get("url"),
"raw_file": item.get("raw_file"),
"normalized_file": item.get("normalized_file"),
"skip_reason": item.get("skip_reason"),
"chars": item.get("chars") or quality.get("chars"),
"quality": {
"content_lines": quality.get("content_lines"),
"prose_lines": quality.get("prose_lines"),
"word_count": quality.get("word_count"),
"is_content": quality.get("is_content"),
},
}
def official_docs_pages(params: dict[str, list[str]]) -> dict:
manifest = load_json(OFFICIAL_NORMALIZED_MANIFEST)
rows = []
for index, item in enumerate(manifest.get("pages") or []):
rows.append(official_doc_row(item, "passed", index))
for index, item in enumerate(manifest.get("skipped_pages") or []):
rows.append(official_doc_row(item, "skipped", index))
status_filter = (params.get("status") or ["all"])[0]
source_filter = (params.get("source_type") or ["all"])[0]
query = (params.get("q") or [""])[0].casefold().strip()
if status_filter != "all":
rows = [row for row in rows if row["status"] == status_filter]
if source_filter != "all":
rows = [row for row in rows if row["source_type"] == source_filter]
if query:
rows = [
row
for row in rows
if query in str(row.get("title") or "").casefold()
or query in str(row.get("url") or "").casefold()
or query in str(row.get("skip_reason") or "").casefold()
]
limit = max(1, min(int((params.get("limit") or ["200"])[0]), 1000))
source_types = sorted({str(row.get("source_type") or "") for row in rows if row.get("source_type")})
return {
"schema": "onec_official_docs_pages.v1",
"manifest": str(OFFICIAL_NORMALIZED_MANIFEST),
"counts": {
"passed": len(manifest.get("pages") or []),
"skipped": len(manifest.get("skipped_pages") or []),
"filtered": len(rows),
},
"source_types": source_types,
"rows": rows[:limit],
}
def safe_child(parent: Path, filename: str) -> Path:
if not filename or "/" in filename or "\\" in filename:
raise ValueError("Invalid file name")
path = (parent / filename).resolve()
parent_resolved = parent.resolve()
if not str(path).casefold().startswith(str(parent_resolved).casefold()):
raise ValueError("Path is outside allowed directory")
return path
def official_docs_page_text(params: dict[str, list[str]]) -> dict:
normalized_file = (params.get("normalized_file") or [""])[0]
raw_file = (params.get("raw_file") or [""])[0]
title = (params.get("title") or [""])[0]
source_type = (params.get("source_type") or [""])[0]
if normalized_file:
path = safe_child(OFFICIAL_DOCS / "normalized", normalized_file)
if not path.exists():
raise ValueError(f"Normalized file does not exist: {normalized_file}")
text = path.read_text(encoding="utf-8-sig", errors="ignore")
return {"schema": "onec_official_docs_page_text.v1", "kind": "normalized", "path": str(path), "text": text[:20000]}
if not raw_file:
raise ValueError("raw_file or normalized_file is required")
path = safe_child(OFFICIAL_DOCS / "raw", raw_file)
if not path.exists():
raise ValueError(f"Raw file does not exist: {raw_file}")
raw_bytes = path.read_bytes()
record = {"content_type": "text/html; charset=Windows-1251"}
raw_text = decode_html(raw_bytes, record)
extractor = TextExtractor()
extractor.feed(raw_text)
extracted = extractor.text()
cleaned = clean_its_text(extracted, title or path.stem, source_type)
return {
"schema": "onec_official_docs_page_text.v1",
"kind": "raw_cleaned_preview",
"path": str(path),
"text": (cleaned or extracted)[:20000],
}
def run_job(job_id: str, command_id: str) -> None:
spec = COMMANDS[command_id]
started = now_iso()
with JOB_LOCK:
JOBS[job_id].update({"status": "running", "started_at": started})
try:
result = subprocess.run(
spec["command"],
cwd=ROOT,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
timeout=int(spec.get("timeout") or 120),
check=False,
)
status = "ok" if result.returncode == 0 else "failed"
payload = {
"status": status,
"finished_at": now_iso(),
"returncode": result.returncode,
"stdout": result.stdout[-12000:],
"stderr": result.stderr[-12000:],
}
except subprocess.TimeoutExpired as exc:
payload = {
"status": "timeout",
"finished_at": now_iso(),
"returncode": None,
"stdout": (exc.stdout or "")[-12000:] if isinstance(exc.stdout, str) else "",
"stderr": (exc.stderr or "")[-12000:] if isinstance(exc.stderr, str) else "",
}
except Exception as exc: # noqa: BLE001 - local console must report all failures.
payload = {
"status": "failed",
"finished_at": now_iso(),
"returncode": None,
"stdout": "",
"stderr": str(exc),
}
with JOB_LOCK:
JOBS[job_id].update(payload)
def run_custom_job(job_id: str, command: list[str]) -> None:
started = now_iso()
with JOB_LOCK:
JOBS[job_id].update({"status": "running", "started_at": started})
try:
result = subprocess.run(
command,
cwd=ROOT,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
timeout=1200,
check=False,
)
status = "ok" if result.returncode == 0 else "failed"
payload = {
"status": status,
"finished_at": now_iso(),
"returncode": result.returncode,
"stdout": result.stdout[-12000:],
"stderr": result.stderr[-12000:],
}
except subprocess.TimeoutExpired as exc:
payload = {
"status": "timeout",
"finished_at": now_iso(),
"returncode": None,
"stdout": (exc.stdout or "")[-12000:] if isinstance(exc.stdout, str) else "",
"stderr": (exc.stderr or "")[-12000:] if isinstance(exc.stderr, str) else "",
}
except Exception as exc: # noqa: BLE001
payload = {"status": "failed", "finished_at": now_iso(), "returncode": None, "stdout": "", "stderr": str(exc)}
with JOB_LOCK:
JOBS[job_id].update(payload)
def start_job(command_id: str) -> dict:
if command_id not in COMMANDS:
raise ValueError(f"Unknown command: {command_id}")
job_id = str(uuid.uuid4())
spec = COMMANDS[command_id]
job = {
"id": job_id,
"command_id": command_id,
"label": spec["label"],
"category": spec["category"],
"command": spec["command"],
"status": "queued",
"created_at": now_iso(),
}
with JOB_LOCK:
JOBS[job_id] = job
thread = threading.Thread(target=run_job, args=(job_id, command_id), daemon=True)
thread.start()
return job
def start_target_its_job(url: str, *, max_pages: int = 12, max_depth: int = 2) -> dict:
parsed = urlparse(url)
if parsed.scheme not in {"http", "https"} or parsed.netloc.lower() != "its.1c.ru":
raise ValueError("Only https://its.1c.ru/... URLs are allowed")
if "/db/" not in parsed.path:
raise ValueError("Only 1C:ITS /db/... documentation URLs are allowed")
max_pages = max(1, min(max_pages, 50))
max_depth = max(0, min(max_depth, 4))
command = [
"powershell",
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-File",
"scripts/run_1c_its_target_doc_pipeline.ps1",
"-Url",
url,
"-MaxPages",
str(max_pages),
"-MaxDepth",
str(max_depth),
]
job_id = str(uuid.uuid4())
job = {
"id": job_id,
"command_id": "official_docs_fetch_target",
"label": "Fetch target 1C:ITS document",
"category": "rag",
"command": command,
"status": "queued",
"created_at": now_iso(),
}
with JOB_LOCK:
JOBS[job_id] = job
thread = threading.Thread(target=run_custom_job, args=(job_id, command), daemon=True)
thread.start()
return job
def write_json(handler: BaseHTTPRequestHandler, status: int, data: dict) -> None:
encoded = json.dumps(data, ensure_ascii=False, indent=2).encode("utf-8")
handler.send_response(status)
handler.send_header("Content-Type", "application/json; charset=utf-8")
handler.send_header("Content-Length", str(len(encoded)))
handler.end_headers()
handler.wfile.write(encoded)
def read_body(handler: BaseHTTPRequestHandler) -> dict:
length = int(handler.headers.get("Content-Length") or 0)
if not length:
return {}
raw = handler.rfile.read(length).decode("utf-8")
return json.loads(raw) if raw.strip() else {}
def static_path(raw_path: str) -> Path | None:
path = unquote(urlparse(raw_path).path)
if path == "/":
path = "/index.html"
candidate = (STATIC_DIR / path.lstrip("/")).resolve()
static_root = STATIC_DIR.resolve()
if not str(candidate).casefold().startswith(str(static_root).casefold()):
return None
if candidate.is_file():
return candidate
return None
def official_docs_static_path(raw_path: str) -> Path | None:
path = unquote(urlparse(raw_path).path)
prefix = "/official-docs-static"
if not path.startswith(prefix):
return None
relative = path[len(prefix) :].lstrip("/") or "index.html"
candidate = (OFFICIAL_STATIC_DIR / relative).resolve()
static_root = OFFICIAL_STATIC_DIR.resolve()
if not str(candidate).casefold().startswith(str(static_root).casefold()):
return None
if candidate.is_file():
return candidate
return None
class ConsoleHandler(BaseHTTPRequestHandler):
def do_GET(self) -> None:
parsed = urlparse(self.path)
if parsed.path == "/api/overview":
write_json(self, 200, overview())
return
if parsed.path == "/api/official-docs/pages":
write_json(self, 200, official_docs_pages(parse_qs(parsed.query)))
return
if parsed.path == "/api/official-docs/page-text":
write_json(self, 200, official_docs_page_text(parse_qs(parsed.query)))
return
if parsed.path.startswith("/api/jobs/"):
job_id = parsed.path.rsplit("/", 1)[-1]
with JOB_LOCK:
job = JOBS.get(job_id)
write_json(self, 200 if job else 404, {"job": job})
return
official_path = official_docs_static_path(self.path)
if official_path:
data = official_path.read_bytes()
content_type = mimetypes.guess_type(str(official_path))[0] or "application/octet-stream"
self.send_response(200)
self.send_header("Content-Type", content_type)
self.send_header("Content-Length", str(len(data)))
self.end_headers()
self.wfile.write(data)
return
path = static_path(self.path)
if path:
data = path.read_bytes()
content_type = mimetypes.guess_type(str(path))[0] or "application/octet-stream"
self.send_response(200)
self.send_header("Content-Type", content_type)
self.send_header("Content-Length", str(len(data)))
self.end_headers()
self.wfile.write(data)
return
write_json(self, 404, {"error": "not found"})
def do_POST(self) -> None:
try:
if urlparse(self.path).path == "/api/rag/query":
write_json(self, 200, query_rag(read_body(self)))
return
if urlparse(self.path).path == "/api/1c/fact":
write_json(self, 200, query_1c_fact(read_body(self)))
return
if urlparse(self.path).path == "/api/1c/route":
write_json(self, 200, query_1c_route(read_body(self)))
return
if urlparse(self.path).path == "/api/1c/intake":
write_json(self, 200, query_1c_intake(read_body(self)))
return
if urlparse(self.path).path == "/api/run":
payload = read_body(self)
job = start_job(str(payload.get("command_id") or ""))
write_json(self, 202, {"job": job})
return
if urlparse(self.path).path == "/api/official-docs/fetch-target":
payload = read_body(self)
job = start_target_its_job(
str(payload.get("url") or ""),
max_pages=int(payload.get("max_pages") or 12),
max_depth=int(payload.get("max_depth") or 2),
)
write_json(self, 202, {"job": job})
return
write_json(self, 404, {"error": "not found"})
except Exception as exc: # noqa: BLE001
write_json(self, 400, {"error": str(exc)})
def log_message(self, format: str, *args) -> None: # noqa: A003
print(f"[management-console] {self.address_string()} - {format % args}")
def main() -> int:
parser = argparse.ArgumentParser(description="Run local LLM/1C management console.")
parser.add_argument("--host", default=DEFAULT_HOST)
parser.add_argument("--port", type=int, default=DEFAULT_PORT)
args = parser.parse_args()
server = ThreadingHTTPServer((args.host, args.port), ConsoleHandler)
print(f"Management console: http://{args.host}:{args.port}/")
try:
server.serve_forever()
except KeyboardInterrupt:
return 130
return 0
if __name__ == "__main__":
raise SystemExit(main())