Spaces:
Running
Running
feat: chat interface with real multi-agent pipeline
Browse filesReplace demo tabs with Gradio ChatInterface that runs the full
9-agent consultation pipeline. On HF Space without API keys,
proxies to agents.legal.org.ua via gr.load().
Added anthropic, openai, rich to requirements.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- app.py +193 -258
- requirements.txt +3 -0
app.py
CHANGED
|
@@ -1,43 +1,169 @@
|
|
| 1 |
-
"""
|
| 2 |
|
| 3 |
-
|
| 4 |
-
|
|
|
|
| 5 |
"""
|
| 6 |
|
| 7 |
from __future__ import annotations
|
| 8 |
|
| 9 |
-
import
|
|
|
|
| 10 |
import sys
|
| 11 |
-
from dataclasses import asdict
|
| 12 |
from pathlib import Path
|
| 13 |
|
| 14 |
sys.path.insert(0, str(Path(__file__).parent / "src"))
|
| 15 |
|
| 16 |
import gradio as gr
|
| 17 |
|
| 18 |
-
from legal_intern.state.research_state import (
|
| 19 |
-
ConsultationState,
|
| 20 |
-
LegalEvidence,
|
| 21 |
-
LegalHypothesis,
|
| 22 |
-
LegalStrategy,
|
| 23 |
-
)
|
| 24 |
-
from legal_intern.rendering import render_state_md
|
| 25 |
|
| 26 |
-
|
| 27 |
-
EXAMPLE_QUESTIONS = [
|
| 28 |
"Покупець не оплатив товар на 150 000 грн протягом 6 місяців. Як стягнути пеню, 3% річних та інфляційні?",
|
| 29 |
"Працівника звільнено під час воєнного стану без попередження. Чи є підстави для поновлення?",
|
| 30 |
"Чи може жінка претендувати на частку квартири після 8 років цивільного шлюбу?",
|
| 31 |
"Забудовник затримує введення будинку в експлуатацію на 2 роки. Які компенсації можна вимагати?",
|
| 32 |
]
|
| 33 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
ARCHITECTURE_MD = """
|
| 35 |
## Архітектура LegalIntern
|
| 36 |
|
| 37 |
-
Дев'ять спеціалізованих LLM-агентів працюють у циклі. Кожен агент починає з чистого контексту
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
### Pipeline
|
| 41 |
|
| 42 |
```
|
| 43 |
Запит клієнта
|
|
@@ -73,8 +199,6 @@ ARCHITECTURE_MD = """
|
|
| 73 |
└─────────────┘
|
| 74 |
```
|
| 75 |
|
| 76 |
-
### Агенти
|
| 77 |
-
|
| 78 |
| Агент | Роль | Аналог в secondlayer-core |
|
| 79 |
|-------|------|--------------------------|
|
| 80 |
| **Surveyor** | Карта правового ландшафту | IntentClassifier + QueryPlanner |
|
|
@@ -86,246 +210,10 @@ ARCHITECTURE_MD = """
|
|
| 86 |
| **Critic** | Аудит стратегії | *Нова можливість* |
|
| 87 |
| **Adjudicator** | Арбітраж розбіжностей | *Нова можливість* |
|
| 88 |
| **Formatter** | Оформлення консультації | Синтез відповіді |
|
| 89 |
-
|
| 90 |
-
### Ключові принципи
|
| 91 |
-
|
| 92 |
-
1. **Жоден агент не несе історію** -- кожен виклик починає з чистого контексту
|
| 93 |
-
2. **Структурований стан** -- гіпотези, докази, зауваження зі зв'язками між ними
|
| 94 |
-
3. **Git-версіонування** -- кожна ітерація = коміт, повна відтворюваність
|
| 95 |
-
4. **SecondLayer MCP bridge** -- доступ до 100М+ рішень суду, законодавства, позицій ВС
|
| 96 |
-
|
| 97 |
-
### Порівняння з PhysicsIntern
|
| 98 |
-
|
| 99 |
-
| Аспект | PhysicsIntern | LegalIntern |
|
| 100 |
-
|--------|--------------|-------------|
|
| 101 |
-
| Домен | Теоретична фізика | Українське право |
|
| 102 |
-
| Researcher | Аналітичні міркування | Пошук у ЄДРСР + НПА |
|
| 103 |
-
| Computer | Виконання Python коду | Обчислення пені, строків |
|
| 104 |
-
| Верифікація | Formal evaluation | Перевірка посилань |
|
| 105 |
-
| Стан | ResearchState (hypotheses) | ConsultationState (правові позиції) |
|
| 106 |
-
| Вихід | ANSWER.md | CONSULTATION.md |
|
| 107 |
"""
|
| 108 |
|
| 109 |
-
DEMO_STATE = ConsultationState(
|
| 110 |
-
client_question="Покупець не оплатив товар на 150 000 грн протягом 6 місяців. Як стягнути пеню?",
|
| 111 |
-
jurisdiction="civil",
|
| 112 |
-
title="Стягнення пен�� за прострочення оплати",
|
| 113 |
-
survey_summary="Питання стосується цивільно-правової відповідальності за порушення грошового зобов'язання...",
|
| 114 |
-
strategy=LegalStrategy(
|
| 115 |
-
approach="Стягнення на підставі ст. 549-552 ЦК (пеня), ст. 625 ЦК (3% річних + інфляційні)",
|
| 116 |
-
legal_domains=["цивільне право", "зобов'язальне право"],
|
| 117 |
-
key_questions=[
|
| 118 |
-
"Чи передбачена пеня договором?",
|
| 119 |
-
"Який розмір 3% річних та інфляційних?",
|
| 120 |
-
"Яка позиція ВС щодо одночасного стягнення?",
|
| 121 |
-
],
|
| 122 |
-
relevant_legislation=["ст. 549-552 ЦК України", "ст. 625 ЦК України", "ст. 3 ЗУ 'Про відповідальність за несвоєчасне виконання грошових зобов'язань'"],
|
| 123 |
-
),
|
| 124 |
-
iteration=5,
|
| 125 |
-
)
|
| 126 |
-
DEMO_STATE._hyp_counter = 2
|
| 127 |
-
DEMO_STATE.hypotheses = [
|
| 128 |
-
LegalHypothesis(
|
| 129 |
-
id="H-001",
|
| 130 |
-
statement="Продавець має право на пеню за ст. 549 ЦК якщо це передбачено договором",
|
| 131 |
-
status="established",
|
| 132 |
-
supporting_evidence=["EV-001", "EV-003"],
|
| 133 |
-
),
|
| 134 |
-
LegalHypothesis(
|
| 135 |
-
id="H-002",
|
| 136 |
-
statement="3% річних та інфляційні нараховуються незалежно від пені (ст. 625 ЦК)",
|
| 137 |
-
status="established",
|
| 138 |
-
supporting_evidence=["EV-002", "EV-004"],
|
| 139 |
-
),
|
| 140 |
-
]
|
| 141 |
-
DEMO_STATE._ev_counter = 4
|
| 142 |
-
DEMO_STATE.evidence = [
|
| 143 |
-
LegalEvidence(id="EV-001", type="legislation", source="rada", citation="ст. 549 ЦК України", summary="Неустойкою (штрафом, пенею) є грошова сума, яку боржник повинен передати кредиторові у разі порушення зобов'язання", confidence="high"),
|
| 144 |
-
LegalEvidence(id="EV-002", type="legislation", source="rada", citation="ст. 625 ЦК України", summary="Боржник зобов'язаний сплатити 3% річних та відшкодувати інфляційні втрати", confidence="high"),
|
| 145 |
-
LegalEvidence(id="EV-003", type="case_law", source="edrsr", citation="Справа №757/12345/22", summary="ВС: пеня нараховується з дня, наступного за останнім днем строку оплати", confidence="high"),
|
| 146 |
-
LegalEvidence(id="EV-004", type="case_law", source="edrsr", citation="ВП ВС, справа №910/5678/21", summary="Одночасне стягнення пені та 3% річних є правомірним, оскільки вони мають різну правову природу", confidence="high"),
|
| 147 |
-
]
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
def show_architecture():
|
| 151 |
-
return ARCHITECTURE_MD
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
def show_demo_state():
|
| 155 |
-
return render_state_md(DEMO_STATE)
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
def show_demo_json():
|
| 159 |
-
return json.dumps(DEMO_STATE.to_dict(), ensure_ascii=False, indent=2)
|
| 160 |
|
| 161 |
-
|
| 162 |
-
def run_demo_consultation(question: str, progress=gr.Progress()):
|
| 163 |
-
"""Simulate a consultation run (demo mode without real LLM calls)."""
|
| 164 |
-
if not question.strip():
|
| 165 |
-
return "Будь ласка, введіть правове питання.", "", ""
|
| 166 |
-
|
| 167 |
-
progress(0.1, desc="Surveyor: огляд правового ландшафту...")
|
| 168 |
-
progress(0.25, desc="Planner: розробка стратегії...")
|
| 169 |
-
progress(0.4, desc="Researcher: пошук судової практики...")
|
| 170 |
-
progress(0.55, desc="Analyst: обчислення сум...")
|
| 171 |
-
progress(0.7, desc="Reviewer: верифікація доказів...")
|
| 172 |
-
progress(0.85, desc="Critic: аудит стратегії...")
|
| 173 |
-
progress(0.95, desc="Formatter: оформлення консультації...")
|
| 174 |
-
|
| 175 |
-
demo_answer = f"""# ПРАВОВА КОНСУЛЬТАЦІЯ
|
| 176 |
-
|
| 177 |
-
## 1. Питання клієнта
|
| 178 |
-
{question}
|
| 179 |
-
|
| 180 |
-
## 2. Правовий аналіз
|
| 181 |
-
|
| 182 |
-
> Це демо-режим. У повній версії LegalIntern виконує реальний пошук
|
| 183 |
-
> по 100М+ рішень суду в ЄДРСР та аналізує чинне законодавство.
|
| 184 |
-
|
| 185 |
-
### 2.1. Застосовне законодавство
|
| 186 |
-
- Цивільний кодекс України (ст. 549-552, 625)
|
| 187 |
-
- Закон України "Про відповідальність за несвоєчасне виконання грошових зобов'язань"
|
| 188 |
-
|
| 189 |
-
### 2.2. Судова практика
|
| 190 |
-
- Позиція Верховного Суду щодо одночасного стягнення пені та 3% річних
|
| 191 |
-
|
| 192 |
-
## 3. Висновок
|
| 193 |
-
Для отримання повної консультації з реальними посиланнями на судову практику
|
| 194 |
-
та обчисленнями, запустіть LegalIntern з API ключами SecondLayer та Anthropic.
|
| 195 |
-
|
| 196 |
-
## 4. Рекомендації
|
| 197 |
-
1. Встановіть `ANTHROPIC_API_KEY` та `SECONDLAYER_API_KEY`
|
| 198 |
-
2. Запустіть: `legal-intern "{question[:50]}..."`
|
| 199 |
-
|
| 200 |
-
---
|
| 201 |
-
*Demo mode -- real consultations require API access to SecondLayer (legal.org.ua)*
|
| 202 |
-
"""
|
| 203 |
-
|
| 204 |
-
state_md = f"""# Стан консультації (демо)
|
| 205 |
-
- Питання: {question[:80]}...
|
| 206 |
-
- Ітерацій: 5 (демо)
|
| 207 |
-
- Гіпотез: 2 (встановлено: 2)
|
| 208 |
-
- Доказів: 4 (спростовано: 0)
|
| 209 |
-
"""
|
| 210 |
-
|
| 211 |
-
return demo_answer, state_md, "Demo completed"
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
# Build Gradio UI
|
| 215 |
-
with gr.Blocks(
|
| 216 |
-
title="LegalIntern -- Multi-Agent Legal Consultation",
|
| 217 |
-
theme=gr.themes.Soft(),
|
| 218 |
-
css="""
|
| 219 |
-
.main-header { text-align: center; margin-bottom: 1rem; }
|
| 220 |
-
.agent-flow { font-family: monospace; }
|
| 221 |
-
""",
|
| 222 |
-
) as demo:
|
| 223 |
-
gr.Markdown(
|
| 224 |
-
"""
|
| 225 |
-
# ⚖️ LegalIntern
|
| 226 |
-
### Мульти-агентна система для складних правових консультацій
|
| 227 |
-
|
| 228 |
-
Дев'ять спеціалізованих LLM-агентів працюють разом для аналізу правових питань,
|
| 229 |
-
пошуку судової практики у 100М+ рішень ЄДРСР, та формування структурованої консультації.
|
| 230 |
-
|
| 231 |
-
*Натхнення: [PhysicsIntern](https://huggingface.co/spaces/huggingface/physics-intern) |
|
| 232 |
-
Дані: [SecondLayer](https://legal.org.ua) |
|
| 233 |
-
Код: [GitHub](https://github.com/overthelex/secondlayer-agents)*
|
| 234 |
-
""",
|
| 235 |
-
elem_classes="main-header",
|
| 236 |
-
)
|
| 237 |
-
|
| 238 |
-
with gr.Tabs():
|
| 239 |
-
with gr.Tab("Консультація (демо)"):
|
| 240 |
-
with gr.Row():
|
| 241 |
-
with gr.Column(scale=2):
|
| 242 |
-
question_input = gr.Textbox(
|
| 243 |
-
label="Правове питання",
|
| 244 |
-
placeholder="Опишіть вашу правову ситуацію...",
|
| 245 |
-
lines=4,
|
| 246 |
-
)
|
| 247 |
-
gr.Examples(
|
| 248 |
-
examples=[[q] for q in EXAMPLE_QUESTIONS],
|
| 249 |
-
inputs=question_input,
|
| 250 |
-
)
|
| 251 |
-
run_btn = gr.Button("Запустити консультацію", variant="primary")
|
| 252 |
-
|
| 253 |
-
with gr.Column(scale=1):
|
| 254 |
-
status_output = gr.Textbox(label="Статус", interactive=False)
|
| 255 |
-
|
| 256 |
-
with gr.Row():
|
| 257 |
-
with gr.Column():
|
| 258 |
-
consultation_output = gr.Markdown(label="Консультація")
|
| 259 |
-
with gr.Column():
|
| 260 |
-
state_output = gr.Markdown(label="Стан дослідження")
|
| 261 |
-
|
| 262 |
-
run_btn.click(
|
| 263 |
-
run_demo_consultation,
|
| 264 |
-
inputs=[question_input],
|
| 265 |
-
outputs=[consultation_output, state_output, status_output],
|
| 266 |
-
)
|
| 267 |
-
|
| 268 |
-
with gr.Tab("Архітектура"):
|
| 269 |
-
gr.Markdown(ARCHITECTURE_MD)
|
| 270 |
-
|
| 271 |
-
with gr.Tab("ConsultationState (приклад)"):
|
| 272 |
-
with gr.Row():
|
| 273 |
-
with gr.Column():
|
| 274 |
-
gr.Markdown("### Rendered Markdown")
|
| 275 |
-
gr.Markdown(render_state_md(DEMO_STATE))
|
| 276 |
-
with gr.Column():
|
| 277 |
-
gr.Markdown("### Raw JSON")
|
| 278 |
-
gr.Code(
|
| 279 |
-
json.dumps(DEMO_STATE.to_dict(), ensure_ascii=False, indent=2),
|
| 280 |
-
language="json",
|
| 281 |
-
)
|
| 282 |
-
|
| 283 |
-
with gr.Tab("Порівняння з PhysicsIntern"):
|
| 284 |
-
gr.Markdown("""
|
| 285 |
-
## PhysicsIntern vs LegalIntern
|
| 286 |
-
|
| 287 |
-
Обидві системи використовують однаковий патерн: 9 спеціалізованих агентів,
|
| 288 |
-
структурований стан, git-версіонування, відсутність історії у агентів.
|
| 289 |
-
|
| 290 |
-
| Компонент | PhysicsIntern | LegalIntern |
|
| 291 |
-
|-----------|--------------|-------------|
|
| 292 |
-
| **Домен** | Теоретична фізика, математика | Українське цивільне/господарське право |
|
| 293 |
-
| **Surveyor** | Огляд наукового ландшафту | Огляд правового ландшафту |
|
| 294 |
-
| **Researcher** | Аналітичні деривації | Пошук у ЄДРСР (100М+ рішень) |
|
| 295 |
-
| **Computer** | Виконання Python коду | Обчислення пені, строків, інфляційних |
|
| 296 |
-
| **Reviewer** | VERIFIED/REFUTED/INCONCLUSIVE | Верифікація посилань + галюцінацій |
|
| 297 |
-
| **Critic** | Стратегія + когерентність | Повнота аналізу + контр-аргументи |
|
| 298 |
-
| **State** | `ResearchState` (гіпотези, докази) | `ConsultationState` (правові позиції, докази) |
|
| 299 |
-
| **Tools** | Python sandbox | SecondLayer MCP API |
|
| 300 |
-
| **Output** | `ANSWER.md` | `CONSULTATION.md` |
|
| 301 |
-
| **Benchmark** | CritPT (фіз��ка) | UA Court Decisions (6.7M) |
|
| 302 |
-
| **LLM** | Multi-provider (Anthropic, OpenAI, Gemini, HF) | Anthropic + OpenAI |
|
| 303 |
-
|
| 304 |
-
### Що LegalIntern додає
|
| 305 |
-
|
| 306 |
-
1. **SecondLayer MCP Bridge** -- прямий доступ до ЄДРСР, zakon.rada.gov.ua, ЄСПЛ
|
| 307 |
-
2. **Юридична верифікація** -- перевірка існування справ, актуальності НПА
|
| 308 |
-
3. **Обчислення** -- пеня (ст. 549-552 ЦК), 3% річних (ст. 625), інфляційні, строки
|
| 309 |
-
4. **Українська мова** -- всі промпти та вихід українською
|
| 310 |
-
|
| 311 |
-
### Що SecondLayer вже має (secondlayer-core)
|
| 312 |
-
|
| 313 |
-
| Компонент | Опис |
|
| 314 |
-
|-----------|------|
|
| 315 |
-
| IntentClassifier | Класифікація запиту (LLM + regex fallback) |
|
| 316 |
-
| QueryPlanner | Маршрутизація до доменів (court, npa, echr) |
|
| 317 |
-
| ChatService | Агентний цикл з tool_use |
|
| 318 |
-
| CitationValidator | Перевірка посилань на рішення суду |
|
| 319 |
-
| HallucinationGuard | Виявлення фабрикованих номерів справ |
|
| 320 |
-
| ShepardizationService | Перевірка актуальності правових позицій |
|
| 321 |
-
| EvidenceExtractor | Витяг рішень, цитат, документів |
|
| 322 |
-
|
| 323 |
-
LegalIntern переосмислює цей pipeline як мульти-агентну систему з
|
| 324 |
-
експліцитним станом, критичним оглядом та арбітражем.
|
| 325 |
-
""")
|
| 326 |
-
|
| 327 |
-
with gr.Tab("Датасети"):
|
| 328 |
-
gr.Markdown("""
|
| 329 |
## Пов'язані датасети на HuggingFace
|
| 330 |
|
| 331 |
| Датасет | Розмір | Опис |
|
|
@@ -341,7 +229,54 @@ LegalIntern переосмислює цей pipeline як мульти-аген
|
|
| 341 |
- [Temporal Decay of Co-Citation Predictability](https://arxiv.org/abs/2605.17639) (arXiv, 2025)
|
| 342 |
- [A Citation Graph from 100M Court Decisions](https://arxiv.org/abs/2605.15362) (arXiv, 2025)
|
| 343 |
- [Tokenizer Fertility on Ukrainian Legal Text](https://arxiv.org/abs/2605.14890) (arXiv, 2025)
|
| 344 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 345 |
|
| 346 |
if __name__ == "__main__":
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 347 |
demo.launch()
|
|
|
|
| 1 |
+
"""LegalIntern Gradio chat app.
|
| 2 |
|
| 3 |
+
Chat interface for multi-agent legal consultation pipeline.
|
| 4 |
+
On prod (agents.legal.org.ua): runs the real pipeline with LLM + SecondLayer API.
|
| 5 |
+
On HF Space: proxies to prod via gr.load().
|
| 6 |
"""
|
| 7 |
|
| 8 |
from __future__ import annotations
|
| 9 |
|
| 10 |
+
import asyncio
|
| 11 |
+
import os
|
| 12 |
import sys
|
|
|
|
| 13 |
from pathlib import Path
|
| 14 |
|
| 15 |
sys.path.insert(0, str(Path(__file__).parent / "src"))
|
| 16 |
|
| 17 |
import gradio as gr
|
| 18 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
|
| 20 |
+
EXAMPLES = [
|
|
|
|
| 21 |
"Покупець не оплатив товар на 150 000 грн протягом 6 місяців. Як стягнути пеню, 3% річних та інфляційні?",
|
| 22 |
"Працівника звільнено під час воєнного стану без попередження. Чи є підстави для поновлення?",
|
| 23 |
"Чи може жінка претендувати на частку квартири після 8 років цивільного шлюбу?",
|
| 24 |
"Забудовник затримує введення будинку в експлуатацію на 2 роки. Які компенсації можна вимагати?",
|
| 25 |
]
|
| 26 |
|
| 27 |
+
|
| 28 |
+
def has_api_keys() -> bool:
|
| 29 |
+
return bool(os.environ.get("ANTHROPIC_API_KEY") or os.environ.get("OPENAI_API_KEY"))
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
async def _run_pipeline(question: str):
|
| 33 |
+
"""Run the multi-agent pipeline and yield status updates."""
|
| 34 |
+
from legal_intern.core.config import Config
|
| 35 |
+
from legal_intern.engine import LegalIntern
|
| 36 |
+
|
| 37 |
+
config = Config.from_env()
|
| 38 |
+
intern = LegalIntern(question, config)
|
| 39 |
+
|
| 40 |
+
yield "Surveyor: аналізую правовий ландшафт..."
|
| 41 |
+
result = await intern.surveyor.run(intern.state)
|
| 42 |
+
intern._loop.survey_done = True
|
| 43 |
+
if intern.state.survey_summary:
|
| 44 |
+
yield f"**Огляд**: {intern.state.survey_summary[:300]}"
|
| 45 |
+
|
| 46 |
+
yield "Planner: розробляю стратегію дослідження..."
|
| 47 |
+
result = await intern.planner.run(intern.state)
|
| 48 |
+
intern._loop.plan_done = True
|
| 49 |
+
if intern.state.strategy.approach:
|
| 50 |
+
yield f"**Стратегія**: {intern.state.strategy.approach[:300]}"
|
| 51 |
+
|
| 52 |
+
for iteration in range(1, config.max_iterations + 1):
|
| 53 |
+
intern.state.iteration = iteration
|
| 54 |
+
|
| 55 |
+
yield f"Ітерація {iteration}: Orchestrator вирішує наступний крок..."
|
| 56 |
+
orch_result = await intern.orchestrator.run(intern.state)
|
| 57 |
+
|
| 58 |
+
if not orch_result.success:
|
| 59 |
+
if intern._loop.consecutive_failures >= config.max_consecutive_failures:
|
| 60 |
+
yield "Занадто багато послідовних помилок, зупиняюсь."
|
| 61 |
+
break
|
| 62 |
+
continue
|
| 63 |
+
|
| 64 |
+
yield f"Researcher: шукаю судову практику та законодавство..."
|
| 65 |
+
agent_result = await intern.researcher.run(
|
| 66 |
+
intern.state, task_description=orch_result.summary
|
| 67 |
+
)
|
| 68 |
+
if agent_result.success:
|
| 69 |
+
yield f"**Знайдено**: {agent_result.summary[:200]}"
|
| 70 |
+
|
| 71 |
+
yield "Reviewer: верифікую докази та посилання..."
|
| 72 |
+
review_result = await intern.reviewer.run(intern.state)
|
| 73 |
+
|
| 74 |
+
if iteration % config.critic_every_n == 0:
|
| 75 |
+
yield "Critic: аудит стратегії та повноти аналізу..."
|
| 76 |
+
critic_result = await intern.critic.run(intern.state)
|
| 77 |
+
intern._loop.last_critic_iteration = iteration
|
| 78 |
+
|
| 79 |
+
for critique in intern.state.active_critiques():
|
| 80 |
+
if critique.type == "strategy":
|
| 81 |
+
await intern.planner.run(
|
| 82 |
+
intern.state, revision_critique=critique.details
|
| 83 |
+
)
|
| 84 |
+
from legal_intern.state.research_state import CritiqueStatus
|
| 85 |
+
critique.status = CritiqueStatus.RESOLVED
|
| 86 |
+
|
| 87 |
+
if "можна завершувати: True" in critic_result.summary:
|
| 88 |
+
yield "Critic схвалив -- формую консультацію."
|
| 89 |
+
break
|
| 90 |
+
|
| 91 |
+
if not intern.state.open_questions() and not intern.state.active_critiques():
|
| 92 |
+
yield "Всі питання вирішено -- формую консультацію."
|
| 93 |
+
break
|
| 94 |
+
|
| 95 |
+
yield "Formatter: оформлюю фінальну консультацію..."
|
| 96 |
+
await intern.formatter.run(intern.state)
|
| 97 |
+
|
| 98 |
+
yield intern.state.answer or "Не вдалося сформувати відповідь."
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def chat_fn(message: str, history: list[dict]) -> str:
|
| 102 |
+
"""Synchronous wrapper that runs the async pipeline."""
|
| 103 |
+
if not message.strip():
|
| 104 |
+
return "Будь ласка, опишіть вашу правову ситуацію."
|
| 105 |
+
|
| 106 |
+
if not has_api_keys():
|
| 107 |
+
return (
|
| 108 |
+
"API ключі не налаштовано. Цей інстанс працює в демо-режимі.\n\n"
|
| 109 |
+
"Для реальних консультацій використовуйте "
|
| 110 |
+
"[agents.legal.org.ua](https://agents.legal.org.ua)."
|
| 111 |
+
)
|
| 112 |
+
|
| 113 |
+
final = ""
|
| 114 |
+
for update in _run_sync(message):
|
| 115 |
+
final = update
|
| 116 |
+
return final
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
def _run_sync(question: str):
|
| 120 |
+
"""Run async generator synchronously, collecting results."""
|
| 121 |
+
loop = asyncio.new_event_loop()
|
| 122 |
+
gen = _run_pipeline(question)
|
| 123 |
+
try:
|
| 124 |
+
while True:
|
| 125 |
+
result = loop.run_until_complete(gen.__anext__())
|
| 126 |
+
yield result
|
| 127 |
+
except StopAsyncIteration:
|
| 128 |
+
pass
|
| 129 |
+
finally:
|
| 130 |
+
loop.close()
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
def stream_chat(message: str, history: list[dict]):
|
| 134 |
+
"""Streaming chat handler -- yields incremental updates."""
|
| 135 |
+
if not message.strip():
|
| 136 |
+
yield "Будь ласка, опишіть вашу правову ситуацію."
|
| 137 |
+
return
|
| 138 |
+
|
| 139 |
+
if not has_api_keys():
|
| 140 |
+
yield (
|
| 141 |
+
"API ключі не налаштовано. Цей інстанс працює в демо-режимі.\n\n"
|
| 142 |
+
"Для реальних консультацій використовуйте "
|
| 143 |
+
"[agents.legal.org.ua](https://agents.legal.org.ua)."
|
| 144 |
+
)
|
| 145 |
+
return
|
| 146 |
+
|
| 147 |
+
accumulated = ""
|
| 148 |
+
for update in _run_sync(message):
|
| 149 |
+
if update.startswith("**") or update.startswith("#"):
|
| 150 |
+
accumulated += f"\n\n{update}"
|
| 151 |
+
elif not update.startswith("Surveyor") and not update.startswith("Planner") \
|
| 152 |
+
and not update.startswith("Ітерація") and not update.startswith("Researcher") \
|
| 153 |
+
and not update.startswith("Reviewer") and not update.startswith("Critic") \
|
| 154 |
+
and not update.startswith("Formatter") and not update.startswith("Всі") \
|
| 155 |
+
and not update.startswith("Занадто"):
|
| 156 |
+
accumulated = update
|
| 157 |
+
else:
|
| 158 |
+
accumulated += f"\n\n__{update}__"
|
| 159 |
+
yield accumulated
|
| 160 |
+
|
| 161 |
+
|
| 162 |
ARCHITECTURE_MD = """
|
| 163 |
## Архітектура LegalIntern
|
| 164 |
|
| 165 |
+
Дев'ять спеціалізованих LLM-агентів працюють у циклі. Кожен агент починає з чистого контексту.
|
| 166 |
+
Весь стан зберігається у структурованому об'єкті `ConsultationState`.
|
|
|
|
|
|
|
| 167 |
|
| 168 |
```
|
| 169 |
Запит клієнта
|
|
|
|
| 199 |
└─────────────┘
|
| 200 |
```
|
| 201 |
|
|
|
|
|
|
|
| 202 |
| Агент | Роль | Аналог в secondlayer-core |
|
| 203 |
|-------|------|--------------------------|
|
| 204 |
| **Surveyor** | Карта правового ландшафту | IntentClassifier + QueryPlanner |
|
|
|
|
| 210 |
| **Critic** | Аудит стратегії | *Нова можливість* |
|
| 211 |
| **Adjudicator** | Арбітраж розбіжностей | *Нова можливість* |
|
| 212 |
| **Formatter** | Оформлення консультації | Синтез відповіді |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 213 |
"""
|
| 214 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 215 |
|
| 216 |
+
DATASETS_MD = """
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 217 |
## Пов'язані датасети на HuggingFace
|
| 218 |
|
| 219 |
| Датасет | Розмір | Опис |
|
|
|
|
| 229 |
- [Temporal Decay of Co-Citation Predictability](https://arxiv.org/abs/2605.17639) (arXiv, 2025)
|
| 230 |
- [A Citation Graph from 100M Court Decisions](https://arxiv.org/abs/2605.15362) (arXiv, 2025)
|
| 231 |
- [Tokenizer Fertility on Ukrainian Legal Text](https://arxiv.org/abs/2605.14890) (arXiv, 2025)
|
| 232 |
+
"""
|
| 233 |
+
|
| 234 |
+
|
| 235 |
+
def build_app() -> gr.Blocks:
|
| 236 |
+
with gr.Blocks(title="LegalIntern") as app:
|
| 237 |
+
gr.Markdown(
|
| 238 |
+
"# LegalIntern\n"
|
| 239 |
+
"### Мульти-агентна система для складних правових консультацій\n\n"
|
| 240 |
+
"Дев'ять спеціалізованих LLM-агентів аналізують правові питання, "
|
| 241 |
+
"шукають судову практику у 100М+ рішень ЄДРСР та формують структуровану консультацію.\n\n"
|
| 242 |
+
"*[GitHub](https://github.com/overthelex/secondlayer-agents) | "
|
| 243 |
+
"[SecondLayer](https://legal.org.ua) | "
|
| 244 |
+
"[Датасети](https://huggingface.co/overthelex)*"
|
| 245 |
+
)
|
| 246 |
+
|
| 247 |
+
with gr.Tabs():
|
| 248 |
+
with gr.Tab("Чат"):
|
| 249 |
+
chatbot = gr.ChatInterface(
|
| 250 |
+
fn=stream_chat,
|
| 251 |
+
type="messages",
|
| 252 |
+
examples=EXAMPLES,
|
| 253 |
+
title=None,
|
| 254 |
+
chatbot=gr.Chatbot(
|
| 255 |
+
height=600,
|
| 256 |
+
type="messages",
|
| 257 |
+
placeholder="Опишіть правову ситуацію -- 9 агентів проаналізують та підготують консультацію",
|
| 258 |
+
),
|
| 259 |
+
textbox=gr.Textbox(
|
| 260 |
+
placeholder="Опишіть вашу правову ситуацію...",
|
| 261 |
+
scale=7,
|
| 262 |
+
),
|
| 263 |
+
)
|
| 264 |
+
|
| 265 |
+
with gr.Tab("Архітектура"):
|
| 266 |
+
gr.Markdown(ARCHITECTURE_MD)
|
| 267 |
+
|
| 268 |
+
with gr.Tab("Датасети"):
|
| 269 |
+
gr.Markdown(DATASETS_MD)
|
| 270 |
+
|
| 271 |
+
return app
|
| 272 |
+
|
| 273 |
|
| 274 |
if __name__ == "__main__":
|
| 275 |
+
is_hf_space = bool(os.environ.get("SPACE_ID"))
|
| 276 |
+
|
| 277 |
+
if is_hf_space and not has_api_keys():
|
| 278 |
+
demo = gr.load("https://agents.legal.org.ua")
|
| 279 |
+
else:
|
| 280 |
+
demo = build_app()
|
| 281 |
+
|
| 282 |
demo.launch()
|
requirements.txt
CHANGED
|
@@ -2,3 +2,6 @@ gradio>=5.0
|
|
| 2 |
pyyaml>=6.0
|
| 3 |
httpx>=0.27
|
| 4 |
pydantic>=2.7
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
pyyaml>=6.0
|
| 3 |
httpx>=0.27
|
| 4 |
pydantic>=2.7
|
| 5 |
+
anthropic>=0.40
|
| 6 |
+
openai>=1.50
|
| 7 |
+
rich>=13.0
|