Spaces:
Running
Running
Ayona commited on
Commit ·
22df219
1
Parent(s): 601f5a4
handoff: harden config and align MVP surface
Browse files
README.md
CHANGED
|
@@ -33,17 +33,19 @@ python_version: "3.11"
|
|
| 33 |
- Можливість додавання коментарів для уточнення контексту
|
| 34 |
- Автоматична класифікація за типом судочинства та категорією
|
| 35 |
|
| 36 |
-
### 🔍 Пошук схожих позицій
|
| 37 |
- Інтелектуальний пошук релевантних правових позицій у базі даних
|
| 38 |
- Пошук на основі згенерованої позиції або вхідного тексту
|
| 39 |
- Гібридний підхід: векторний пошук + BM25
|
| 40 |
- Відображення результатів з посиланнями на джерела
|
|
|
|
| 41 |
|
| 42 |
-
### ⚖️ Порівняльний аналіз
|
| 43 |
- Детальний аналіз релевантності знайдених позицій
|
| 44 |
- Виявлення спільних правових аспектів
|
| 45 |
- Оцінка можливості застосування існуючих позицій
|
| 46 |
- Обґрунтовані висновки щодо необхідності створення нової позиції
|
|
|
|
| 47 |
|
| 48 |
### ⚙️ Редагування промптів
|
| 49 |
- Персональне налаштування промптів для AI
|
|
@@ -153,8 +155,10 @@ REDIS_PASSWORD=your_redis_password
|
|
| 153 |
|
| 154 |
### 4. Запуск додатку
|
| 155 |
|
|
|
|
|
|
|
| 156 |
```bash
|
| 157 |
-
python
|
| 158 |
```
|
| 159 |
|
| 160 |
Додаток буде доступний за адресою: `http://localhost:7860`
|
|
@@ -320,6 +324,12 @@ redis:
|
|
| 320 |
|
| 321 |
## 🚀 Deployment
|
| 322 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 323 |
### Hugging Face Spaces
|
| 324 |
|
| 325 |
1. Створіть новий Space на [Hugging Face](https://huggingface.co/spaces)
|
|
@@ -443,4 +453,4 @@ gunicorn main:app --workers 4 --bind 0.0.0.0:7860
|
|
| 443 |
|
| 444 |
**Останнє оновлення:** 2026-01-03
|
| 445 |
|
| 446 |
-
**Статус:**
|
|
|
|
| 33 |
- Можливість додавання коментарів для уточнення контексту
|
| 34 |
- Автоматична класифікація за типом судочинства та категорією
|
| 35 |
|
| 36 |
+
### 🔍 Пошук схожих позицій *(опціонально)*
|
| 37 |
- Інтелектуальний пошук релевантних правових позицій у базі даних
|
| 38 |
- Пошук на основі згенерованої позиції або вхідного тексту
|
| 39 |
- Гібридний підхід: векторний пошук + BM25
|
| 40 |
- Відображення результатів з посиланнями на джерела
|
| 41 |
+
- Якщо customer build поставляється без retrieval-інфраструктури, цей модуль може бути вимкнений
|
| 42 |
|
| 43 |
+
### ⚖️ Порівняльний аналіз *(опціонально)*
|
| 44 |
- Детальний аналіз релевантності знайдених позицій
|
| 45 |
- Виявлення спільних правових аспектів
|
| 46 |
- Оцінка можливості застосування існуючих позицій
|
| 47 |
- Обґрунтовані висновки щодо необхідності створення нової позиції
|
| 48 |
+
- Не є критерієм приймання MVP, якщо це не зафіксовано окремо в ТЗ
|
| 49 |
|
| 50 |
### ⚙️ Редагування промптів
|
| 51 |
- Персональне налаштування промптів для AI
|
|
|
|
| 155 |
|
| 156 |
### 4. Запуск додатку
|
| 157 |
|
| 158 |
+
Локальний запуск:
|
| 159 |
+
|
| 160 |
```bash
|
| 161 |
+
python app.py
|
| 162 |
```
|
| 163 |
|
| 164 |
Додаток буде доступний за адресою: `http://localhost:7860`
|
|
|
|
| 324 |
|
| 325 |
## 🚀 Deployment
|
| 326 |
|
| 327 |
+
### Deployment profiles
|
| 328 |
+
|
| 329 |
+
- **Customer MVP / handoff:** Generation + model/thinking/comment controls are the primary supported flow.
|
| 330 |
+
- **Optional modules:** Search and comparative analysis may be enabled only when retrieval indexes are available and accepted as in-scope.
|
| 331 |
+
- **Known limitation:** if retrieval uses `DocSA/legal-position-indexes`, that corpus is an older snapshot and may be stale.
|
| 332 |
+
|
| 333 |
### Hugging Face Spaces
|
| 334 |
|
| 335 |
1. Створіть новий Space на [Hugging Face](https://huggingface.co/spaces)
|
|
|
|
| 453 |
|
| 454 |
**Останнє оновлення:** 2026-01-03
|
| 455 |
|
| 456 |
+
**Статус:** 🟡 Handoff Candidate (generation-first MVP; optional retrieval modules require separate validation)
|
app.py
CHANGED
|
@@ -24,98 +24,17 @@ def _suppress_asyncio_fd_errors(unraisable):
|
|
| 24 |
|
| 25 |
sys.unraisablehook = _suppress_asyncio_fd_errors
|
| 26 |
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
os.environ
|
| 30 |
-
|
| 31 |
-
os.environ.setdefault('UVICORN_LOOP', 'asyncio')
|
| 32 |
|
| 33 |
-
# Apply nest_asyncio only if needed (some Python versions have conflicts)
|
| 34 |
-
# try:
|
| 35 |
-
# import nest_asyncio
|
| 36 |
-
# nest_asyncio.apply()
|
| 37 |
-
# except Exception as e:
|
| 38 |
-
# print(f"[WARNING] Could not apply nest_asyncio: {e}")
|
| 39 |
|
| 40 |
# Add project root to Python path
|
| 41 |
project_root = Path(__file__).parent
|
| 42 |
sys.path.insert(0, str(project_root))
|
| 43 |
-
|
| 44 |
-
# ============ Network Diagnostics ============
|
| 45 |
-
def run_network_diagnostics():
|
| 46 |
-
"""Check outbound network connectivity from HF Spaces container."""
|
| 47 |
-
import urllib.request
|
| 48 |
-
import socket
|
| 49 |
-
|
| 50 |
-
print("=" * 50)
|
| 51 |
-
print("🔍 NETWORK DIAGNOSTICS")
|
| 52 |
-
print("=" * 50)
|
| 53 |
-
|
| 54 |
-
# Check proxy env vars
|
| 55 |
-
proxy_vars = ['HTTP_PROXY', 'HTTPS_PROXY', 'http_proxy', 'https_proxy', 'NO_PROXY', 'no_proxy', 'ALL_PROXY']
|
| 56 |
-
print("\n📡 Proxy environment variables:")
|
| 57 |
-
for var in proxy_vars:
|
| 58 |
-
val = os.environ.get(var)
|
| 59 |
-
if val:
|
| 60 |
-
print(f" {var} = {val}")
|
| 61 |
-
if not any(os.environ.get(v) for v in proxy_vars):
|
| 62 |
-
print(" (none set)")
|
| 63 |
-
|
| 64 |
-
# Check DNS resolution
|
| 65 |
-
hosts = ['api.anthropic.com', 'api.openai.com', 'generativelanguage.googleapis.com']
|
| 66 |
-
print("\n🌐 DNS resolution:")
|
| 67 |
-
for host in hosts:
|
| 68 |
-
try:
|
| 69 |
-
ip = socket.gethostbyname(host)
|
| 70 |
-
print(f" ✅ {host} -> {ip}")
|
| 71 |
-
except socket.gaierror as e:
|
| 72 |
-
print(f" ❌ {host} -> DNS FAILED: {e}")
|
| 73 |
-
|
| 74 |
-
# Check actual HTTP(S) connectivity
|
| 75 |
-
print("\n🔌 HTTPS connectivity:")
|
| 76 |
-
test_urls = [
|
| 77 |
-
('https://api.anthropic.com', 'Anthropic API'),
|
| 78 |
-
('https://api.openai.com', 'OpenAI API'),
|
| 79 |
-
('https://httpbin.org/get', 'httpbin (general internet)'),
|
| 80 |
-
]
|
| 81 |
-
for url, name in test_urls:
|
| 82 |
-
try:
|
| 83 |
-
req = urllib.request.Request(url, method='HEAD')
|
| 84 |
-
req.add_header('User-Agent', 'connectivity-test/1.0')
|
| 85 |
-
resp = urllib.request.urlopen(req, timeout=10)
|
| 86 |
-
print(f" ✅ {name} ({url}) -> HTTP {resp.status}")
|
| 87 |
-
except urllib.error.HTTPError as e:
|
| 88 |
-
# HTTP error means we CAN connect (just got an error response)
|
| 89 |
-
print(f" ✅ {name} ({url}) -> HTTP {e.code} (connection OK, auth expected)")
|
| 90 |
-
except Exception as e:
|
| 91 |
-
print(f" ❌ {name} ({url}) -> {type(e).__name__}: {e}")
|
| 92 |
-
|
| 93 |
-
# Check httpx (used by anthropic/openai SDKs)
|
| 94 |
-
print("\n🔧 httpx connectivity test:")
|
| 95 |
-
try:
|
| 96 |
-
import httpx
|
| 97 |
-
print(f" httpx version: {httpx.__version__}")
|
| 98 |
-
# Test with default settings
|
| 99 |
-
with httpx.Client(timeout=10) as client:
|
| 100 |
-
resp = client.get("https://api.anthropic.com")
|
| 101 |
-
print(f" ✅ httpx (default) -> Anthropic HTTP {resp.status_code}")
|
| 102 |
-
# Test OpenAI with HTTP/2 disabled (avoids 421 Misdirected Request)
|
| 103 |
-
with httpx.Client(timeout=10, http2=False) as client:
|
| 104 |
-
resp = client.get("https://api.openai.com")
|
| 105 |
-
print(f" ✅ httpx (http1.1) -> OpenAI HTTP {resp.status_code}")
|
| 106 |
-
# Also test with default HTTP/2 to compare
|
| 107 |
-
try:
|
| 108 |
-
with httpx.Client(timeout=10) as client:
|
| 109 |
-
resp = client.get("https://api.openai.com")
|
| 110 |
-
print(f" ✅ httpx (default) -> OpenAI HTTP {resp.status_code}")
|
| 111 |
-
except Exception as e2:
|
| 112 |
-
print(f" ⚠️ httpx (default/http2) -> OpenAI FAILED: {type(e2).__name__}: {e2}")
|
| 113 |
-
except Exception as e:
|
| 114 |
-
print(f" ❌ httpx -> {type(e).__name__}: {e}")
|
| 115 |
-
|
| 116 |
-
print("=" * 50)
|
| 117 |
-
|
| 118 |
-
# ============ End Diagnostics ============
|
| 119 |
|
| 120 |
# Import and launch interface
|
| 121 |
from interface import create_gradio_interface
|
|
@@ -134,11 +53,8 @@ else:
|
|
| 134 |
demo = create_gradio_interface()
|
| 135 |
|
| 136 |
if __name__ == "__main__":
|
| 137 |
-
# Run diagnostics only when executed directly
|
| 138 |
-
run_network_diagnostics()
|
| 139 |
-
|
| 140 |
print("🚀 Starting Legal Position AI Analyzer...")
|
| 141 |
-
|
| 142 |
# Detect if running on HF Spaces or locally
|
| 143 |
is_hf_space = os.environ.get('SPACE_ID') is not None
|
| 144 |
|
|
|
|
| 24 |
|
| 25 |
sys.unraisablehook = _suppress_asyncio_fd_errors
|
| 26 |
|
| 27 |
+
def configure_runtime_environment() -> None:
|
| 28 |
+
"""Apply runtime defaults for Hugging Face Spaces / Gradio deployment."""
|
| 29 |
+
os.environ.setdefault('GRADIO_SERVER_NAME', '0.0.0.0')
|
| 30 |
+
os.environ.setdefault('GRADIO_SERVER_PORT', '7860')
|
| 31 |
+
os.environ.setdefault('UVICORN_LOOP', 'asyncio')
|
| 32 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
|
| 34 |
# Add project root to Python path
|
| 35 |
project_root = Path(__file__).parent
|
| 36 |
sys.path.insert(0, str(project_root))
|
| 37 |
+
configure_runtime_environment()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
|
| 39 |
# Import and launch interface
|
| 40 |
from interface import create_gradio_interface
|
|
|
|
| 53 |
demo = create_gradio_interface()
|
| 54 |
|
| 55 |
if __name__ == "__main__":
|
|
|
|
|
|
|
|
|
|
| 56 |
print("🚀 Starting Legal Position AI Analyzer...")
|
| 57 |
+
|
| 58 |
# Detect if running on HF Spaces or locally
|
| 59 |
is_hf_space = os.environ.get('SPACE_ID') is not None
|
| 60 |
|
config.py
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
|
|
| 1 |
import os
|
| 2 |
from enum import Enum
|
| 3 |
from pathlib import Path
|
| 4 |
from dotenv import load_dotenv
|
| 5 |
|
|
|
|
|
|
|
| 6 |
# Load environment variables
|
| 7 |
load_dotenv()
|
| 8 |
|
|
@@ -13,19 +16,6 @@ OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
|
|
| 13 |
ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_API_KEY")
|
| 14 |
DEEPSEEK_API_KEY = os.getenv("DEEPSEEK_API_KEY")
|
| 15 |
|
| 16 |
-
# Debug: Print key status (masked)
|
| 17 |
-
print("="*30)
|
| 18 |
-
print("🔧 CONFIGURATION DEBUG")
|
| 19 |
-
print(f"OPENAI_API_KEY: {'✅ Found' if OPENAI_API_KEY else '❌ Missing'}")
|
| 20 |
-
if OPENAI_API_KEY:
|
| 21 |
-
print(f" Length: {len(OPENAI_API_KEY)}")
|
| 22 |
-
print(f" Prefix: {OPENAI_API_KEY[:5]}...")
|
| 23 |
-
|
| 24 |
-
print(f"ANTHROPIC_API_KEY: {'✅ Found' if ANTHROPIC_API_KEY else '❌ Missing'}")
|
| 25 |
-
print(f"DEEPSEEK_API_KEY: {'✅ Found' if DEEPSEEK_API_KEY else '❌ Missing'}")
|
| 26 |
-
print(f"GEMINI_API_KEY: {'✅ Found' if os.getenv('GEMINI_API_KEY') else '❌ Missing'}")
|
| 27 |
-
print("="*30)
|
| 28 |
-
|
| 29 |
# Конфігурація Gemini
|
| 30 |
GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
|
| 31 |
if GEMINI_API_KEY:
|
|
|
|
| 1 |
+
import logging
|
| 2 |
import os
|
| 3 |
from enum import Enum
|
| 4 |
from pathlib import Path
|
| 5 |
from dotenv import load_dotenv
|
| 6 |
|
| 7 |
+
logger = logging.getLogger(__name__)
|
| 8 |
+
|
| 9 |
# Load environment variables
|
| 10 |
load_dotenv()
|
| 11 |
|
|
|
|
| 16 |
ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_API_KEY")
|
| 17 |
DEEPSEEK_API_KEY = os.getenv("DEEPSEEK_API_KEY")
|
| 18 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
# Конфігурація Gemini
|
| 20 |
GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
|
| 21 |
if GEMINI_API_KEY:
|
interface.py
CHANGED
|
@@ -730,8 +730,8 @@ def create_gradio_interface() -> gr.Blocks:
|
|
| 730 |
|
| 731 |
with gr.TabItem("📂 Завантаження файлу", id="file_tab"):
|
| 732 |
file_input = gr.File(
|
| 733 |
-
label="Перетягніть файл або натисніть для вибору",
|
| 734 |
-
file_types=[".txt"
|
| 735 |
file_count="single"
|
| 736 |
)
|
| 737 |
|
|
@@ -752,7 +752,7 @@ def create_gradio_interface() -> gr.Blocks:
|
|
| 752 |
)
|
| 753 |
|
| 754 |
generate_position_button = gr.Button(
|
| 755 |
-
"
|
| 756 |
variant="primary",
|
| 757 |
size="lg"
|
| 758 |
)
|
|
|
|
| 730 |
|
| 731 |
with gr.TabItem("📂 Завантаження файлу", id="file_tab"):
|
| 732 |
file_input = gr.File(
|
| 733 |
+
label="Перетягніть TXT-файл або натисніть для вибору",
|
| 734 |
+
file_types=[".txt"],
|
| 735 |
file_count="single"
|
| 736 |
)
|
| 737 |
|
|
|
|
| 752 |
)
|
| 753 |
|
| 754 |
generate_position_button = gr.Button(
|
| 755 |
+
"📝 Згенерувати правову позицію",
|
| 756 |
variant="primary",
|
| 757 |
size="lg"
|
| 758 |
)
|