Improve document processing pipeline and agent workflows
- Add delete_document_chunks for reprocessing, save extracted text to disk - Expand case directory structure (original/extracted/proofread/backup) - Update classifier patterns (תגובה, הודעת עמדה) - Fix proofreader agent paths for new directory layout - Update HEARTBEAT to notify on every task completion - Improve bidi_table with LRE/PDF directional embedding - Add Paperclip project verification and auto-close setup issue - Add auto-sync-cases.sh for Gitea synchronization Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -69,16 +69,17 @@ python3 /home/chaim/legal-ai/scripts/notify.py \
|
|||||||
"תוכן ההודעה עם סיכום מה נדרש"
|
"תוכן ההודעה עם סיכום מה נדרש"
|
||||||
```
|
```
|
||||||
|
|
||||||
**מתי לשלוח:**
|
**מתי לשלוח — תמיד:**
|
||||||
|
- **סיום כל משימה** — עם סיכום קצר של מה בוצע
|
||||||
- בקשה לקביעת תוצאה (דחייה/קבלה/חלקית)
|
- בקשה לקביעת תוצאה (דחייה/קבלה/חלקית)
|
||||||
- בקשה לאישור כיוון נימוק
|
- בקשה לאישור כיוון נימוק
|
||||||
- דוח QA שנכשל (צריך החלטה על תיקונים)
|
- דוח QA שנכשל (צריך החלטה על תיקונים)
|
||||||
- החלטה מוכנה לביקורת דפנה
|
- החלטה מוכנה לביקורת דפנה
|
||||||
- כל מצב שדורש פעולה אנושית ולא יכול להתקדם לבד
|
- כל מצב שדורש פעולה אנושית ולא יכול להתקדם לבד
|
||||||
|
- שגיאה שלא ניתן לפתור ללא התערבות
|
||||||
|
|
||||||
**מתי לא לשלוח:**
|
**מתי לא לשלוח:**
|
||||||
- עדכוני סטטוס רגילים
|
- עדכוני סטטוס ביניים (רק בסיום)
|
||||||
- סיום משימה שלא דורשת תגובה
|
|
||||||
- שגיאות טכניות שאפשר לפתור לבד
|
- שגיאות טכניות שאפשר לפתור לבד
|
||||||
|
|
||||||
## 6. Release
|
## 6. Release
|
||||||
|
|||||||
@@ -183,9 +183,9 @@ tools:
|
|||||||
1. [שאלה עקרונית — "האם..."]
|
1. [שאלה עקרונית — "האם..."]
|
||||||
2. [שאלה יישומית — "מהם..."]
|
2. [שאלה יישומית — "מהם..."]
|
||||||
|
|
||||||
**מילות מפתח לחיפוש:**
|
**חיפוש תקדימים:**
|
||||||
- nevo: "ביטוי" ו "ביטוי" ו "ועדת ערר"
|
- nevo (קלאסי): "ביטוי" ו "ביטוי" ו "ועדת ערר"
|
||||||
- law-mate: מילה1 מילה2 מילה3
|
- nevo AI / law-mate: [השאלות המשפטיות מלמעלה — שאלה עקרונית + יישומית]
|
||||||
|
|
||||||
**חקיקה רלוונטית:**
|
**חקיקה רלוונטית:**
|
||||||
- סעיף X לחוק...
|
- סעיף X לחוק...
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ tools:
|
|||||||
1. טען את מילון ראשי התיבות: `/home/chaim/legal-ai/data/abbreviations.json`
|
1. טען את מילון ראשי התיבות: `/home/chaim/legal-ai/data/abbreviations.json`
|
||||||
2. **סדר החלפה:** ארוכים לפני קצרים (למניעת החלפה חלקית)
|
2. **סדר החלפה:** ארוכים לפני קצרים (למניעת החלפה חלקית)
|
||||||
3. לכל מסמך:
|
3. לכל מסמך:
|
||||||
- קרא את קובץ ה-MD מהדיסק (מצא אותו ב-`data/cases/` לפי הנתיב)
|
- קרא את קובץ הטקסט מתיקיית `documents/extracted/` בתיק (קובץ `.txt` עם אותו שם כמו ה-PDF המקורי)
|
||||||
- החלף כל מופע של ראשי תיבות שבורים (מפתחות המילון) בצורה הנכונה (ערכי המילון)
|
- החלף כל מופע של ראשי תיבות שבורים (מפתחות המילון) בצורה הנכונה (ערכי המילון)
|
||||||
- ספור כמה החלפות בוצעו
|
- ספור כמה החלפות בוצעו
|
||||||
|
|
||||||
@@ -58,13 +58,13 @@ tools:
|
|||||||
**תקן** רק מה שאתה בטוח בו (90%+). אם לא בטוח — סמן `[?]` ליד המקום הבעייתי.
|
**תקן** רק מה שאתה בטוח בו (90%+). אם לא בטוח — סמן `[?]` ליד המקום הבעייתי.
|
||||||
|
|
||||||
### שלב 4: שמירה
|
### שלב 4: שמירה
|
||||||
1. **גיבוי**: שמור עותק מקורי כ-`{filename}.pre-proofread.md`
|
1. **גיבוי**: העתק את הקובץ המקורי מ-`extracted/` לתיקיית `documents/backup/` עם סיומת `.pre-proofread.txt`
|
||||||
2. **כתוב** את הגרסה המתוקנת לקובץ ה-MD המקורי
|
2. **כתוב** את הגרסה המתוקנת לתיקיית `documents/proofread/` (עם אותו שם קובץ כמו ב-`extracted/`)
|
||||||
3. עדכן את מסד הנתונים — שנה `extraction_status` ל-`proofread`:
|
3. עדכן את מסד הנתונים — שנה `extraction_status` ל-`proofread`:
|
||||||
```bash
|
```bash
|
||||||
PGPASSWORD="${PGPASSWORD:-$(grep DB_PASSWORD /home/chaim/.env | cut -d= -f2)}" \
|
PGPASSWORD="${PGPASSWORD:-$(grep DB_PASSWORD /home/chaim/.env | cut -d= -f2)}" \
|
||||||
psql -h localhost -p 5432 -U "${DB_USER:-legal_ai}" -d "${DB_NAME:-legal_ai}" \
|
psql -h localhost -p 5432 -U "${DB_USER:-legal_ai}" -d "${DB_NAME:-legal_ai}" \
|
||||||
-c "UPDATE documents SET extraction_status = 'proofread', extracted_text = pg_read_file('/path/to/file.md') WHERE id = '{doc_id}';"
|
-c "UPDATE documents SET extraction_status = 'proofread', extracted_text = pg_read_file('/path/to/file.txt') WHERE id = '{doc_id}';"
|
||||||
```
|
```
|
||||||
אם עדכון DB לא אפשרי, עדכן רק את הקובץ ודווח.
|
אם עדכון DB לא אפשרי, עדכן רק את הקובץ ודווח.
|
||||||
|
|
||||||
@@ -90,7 +90,7 @@ psql -h localhost -p 5432 -U "${DB_USER:-legal_ai}" -d "${DB_NAME:-legal_ai}" \
|
|||||||
## כללים קריטיים
|
## כללים קריטיים
|
||||||
|
|
||||||
1. **אל תשנה תוכן משפטי** — רק תיקוני OCR. אם מילה נראית מוזרה אבל היא מונח משפטי — אל תגע
|
1. **אל תשנה תוכן משפטי** — רק תיקוני OCR. אם מילה נראית מוזרה אבל היא מונח משפטי — אל תגע
|
||||||
2. **אל תדרוס בלי גיבוי** — תמיד `.pre-proofread.md` לפני שינוי
|
2. **אל תדרוס בלי גיבוי** — תמיד העתק ל-`backup/` לפני שינוי
|
||||||
3. **ראשי תיבות ארוכים קודם** — `נתבייע` (5 תווים) לפני `עייד` (3 תווים)
|
3. **ראשי תיבות ארוכים קודם** — `נתבייע` (5 תווים) לפני `עייד` (3 תווים)
|
||||||
4. **דווח מקומות מסופקים** — סמן `[?]` ותן לאדם להחליט
|
4. **דווח מקומות מסופקים** — סמן `[?]` ותן לאדם להחליט
|
||||||
5. **אל תמציא טקסט** — אם חסר משהו, סמן `[...]` ואל תנחש
|
5. **אל תמציא טקסט** — אם חסר משהו, סמן `[...]` ואל תנחש
|
||||||
|
|||||||
@@ -687,6 +687,16 @@ async def update_decision(decision_id: UUID, **fields) -> None:
|
|||||||
|
|
||||||
# ── Chunks & Vectors ───────────────────────────────────────────────
|
# ── Chunks & Vectors ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
async def delete_document_chunks(document_id: UUID) -> int:
|
||||||
|
"""Delete all chunks for a document (used before reprocessing)."""
|
||||||
|
pool = await get_pool()
|
||||||
|
async with pool.acquire() as conn:
|
||||||
|
result = await conn.execute(
|
||||||
|
"DELETE FROM document_chunks WHERE document_id = $1", document_id
|
||||||
|
)
|
||||||
|
return int(result.split()[-1]) # e.g. "DELETE 5" -> 5
|
||||||
|
|
||||||
|
|
||||||
async def store_chunks(
|
async def store_chunks(
|
||||||
document_id: UUID,
|
document_id: UUID,
|
||||||
case_id: UUID | None,
|
case_id: UUID | None,
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ logger = logging.getLogger(__name__)
|
|||||||
_FILENAME_RULES: list[tuple[str, str, float]] = [
|
_FILENAME_RULES: list[tuple[str, str, float]] = [
|
||||||
# (regex pattern on filename, doc_type, confidence)
|
# (regex pattern on filename, doc_type, confidence)
|
||||||
(r"כתב.ערר|כתב-ערר", "appeal", 1.0),
|
(r"כתב.ערר|כתב-ערר", "appeal", 1.0),
|
||||||
(r"תשובה|תשובת|תגובת|השלמת.טיעון|בקשה.להשלמת", "response", 1.0),
|
(r"תשובה|תשובת|תגובה|תגובת|השלמת.טיעון|בקשה.להשלמת|הודעת.עמדה", "response", 1.0),
|
||||||
(r"פרוטוקול", "protocol", 1.0),
|
(r"פרוטוקול", "protocol", 1.0),
|
||||||
(r"החלטת?.ביניים|החלטה.לתיקון", "decision", 0.95),
|
(r"החלטת?.ביניים|החלטה.לתיקון", "decision", 0.95),
|
||||||
(r"הוראות.תכנית|תכנית", "plan", 1.0),
|
(r"הוראות.תכנית|תכנית", "plan", 1.0),
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from legal_mcp.services import chunker, db, embeddings, extractor, references_extractor
|
from legal_mcp.services import chunker, db, embeddings, extractor, references_extractor
|
||||||
@@ -37,6 +38,17 @@ async def process_document(document_id: UUID, case_id: UUID) -> dict:
|
|||||||
page_count=page_count,
|
page_count=page_count,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Save extracted text to documents/extracted/ directory
|
||||||
|
original_path = Path(doc["file_path"])
|
||||||
|
extracted_dir = original_path.parent.parent / "extracted"
|
||||||
|
extracted_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
txt_path = extracted_dir / (original_path.stem + ".txt")
|
||||||
|
try:
|
||||||
|
txt_path.write_text(text, encoding="utf-8")
|
||||||
|
logger.info("Saved extracted text to %s", txt_path)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("Failed to save text file (non-fatal): %s", e)
|
||||||
|
|
||||||
# Step 1.5: Classify document — local rules first, Claude Code headless fallback
|
# Step 1.5: Classify document — local rules first, Claude Code headless fallback
|
||||||
classification_result = {}
|
classification_result = {}
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -62,7 +62,12 @@ async def case_create(
|
|||||||
# Initialize git repo for the case
|
# Initialize git repo for the case
|
||||||
case_dir = config.find_case_dir(case_number)
|
case_dir = config.find_case_dir(case_number)
|
||||||
case_dir.mkdir(parents=True, exist_ok=True)
|
case_dir.mkdir(parents=True, exist_ok=True)
|
||||||
(case_dir / "documents").mkdir(exist_ok=True)
|
docs_dir = case_dir / "documents"
|
||||||
|
docs_dir.mkdir(exist_ok=True)
|
||||||
|
(docs_dir / "original").mkdir(exist_ok=True)
|
||||||
|
(docs_dir / "extracted").mkdir(exist_ok=True)
|
||||||
|
(docs_dir / "proofread").mkdir(exist_ok=True)
|
||||||
|
(docs_dir / "backup").mkdir(exist_ok=True)
|
||||||
(case_dir / "drafts").mkdir(exist_ok=True)
|
(case_dir / "drafts").mkdir(exist_ok=True)
|
||||||
|
|
||||||
# Save case metadata
|
# Save case metadata
|
||||||
|
|||||||
37
scripts/auto-sync-cases.sh
Executable file
37
scripts/auto-sync-cases.sh
Executable file
@@ -0,0 +1,37 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Auto-sync case repos to Gitea
|
||||||
|
# Runs via crontab every minute, commits and pushes any changes found.
|
||||||
|
|
||||||
|
CASES_DIR="/home/chaim/legal-ai/data/cases"
|
||||||
|
LOG="/home/chaim/legal-ai/data/.auto-sync.log"
|
||||||
|
GIT_ENV="GIT_AUTHOR_NAME=Ezer Mishpati GIT_AUTHOR_EMAIL=legal@local GIT_COMMITTER_NAME=Ezer Mishpati GIT_COMMITTER_EMAIL=legal@local GIT_TERMINAL_PROMPT=0"
|
||||||
|
|
||||||
|
for status_dir in "$CASES_DIR"/new "$CASES_DIR"/in-progress "$CASES_DIR"/completed; do
|
||||||
|
[ -d "$status_dir" ] || continue
|
||||||
|
for case_dir in "$status_dir"/*/; do
|
||||||
|
[ -d "$case_dir/.git" ] || continue
|
||||||
|
|
||||||
|
cd "$case_dir" || continue
|
||||||
|
|
||||||
|
# Check for any changes (modified, new, deleted)
|
||||||
|
changes=$(git status --porcelain 2>/dev/null)
|
||||||
|
[ -z "$changes" ] && continue
|
||||||
|
|
||||||
|
# Stage all changes
|
||||||
|
git add -A 2>/dev/null
|
||||||
|
|
||||||
|
# Build commit message from changed files
|
||||||
|
changed_files=$(git diff --cached --name-only 2>/dev/null | head -5)
|
||||||
|
count=$(git diff --cached --name-only 2>/dev/null | wc -l)
|
||||||
|
case_name=$(basename "$case_dir")
|
||||||
|
msg="סנכרון אוטומטי — ${count} קבצים שונו"
|
||||||
|
|
||||||
|
# Commit
|
||||||
|
env $GIT_ENV git commit -m "$msg" --quiet 2>/dev/null
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
# Push (non-blocking, ignore errors)
|
||||||
|
git push origin main --quiet 2>/dev/null
|
||||||
|
echo "$(date '+%Y-%m-%d %H:%M:%S') | $case_name | $count files synced" >> "$LOG"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
done
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""BiDi-safe box-drawing table renderer for mixed Hebrew/English terminal output.
|
"""BiDi-safe box-drawing table renderer for mixed Hebrew/English terminal output.
|
||||||
|
|
||||||
Uses LRM (Left-to-Right Mark, U+200E) before box-drawing characters to prevent
|
Uses Unicode directional marks to prevent the BiDi algorithm from breaking
|
||||||
the BiDi algorithm from breaking table alignment when Hebrew text is present.
|
table alignment when Hebrew text is present.
|
||||||
|
|
||||||
Usage as module:
|
Usage as module:
|
||||||
from scripts.bidi_table import bidi_table
|
from scripts.bidi_table import bidi_table
|
||||||
@@ -14,14 +14,25 @@ Usage from CLI:
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
LRM = "\u200E" # Left-to-Right Mark — invisible, prevents BiDi reordering
|
import re
|
||||||
|
|
||||||
|
LRM = "\u200E" # Left-to-Right Mark
|
||||||
|
RLM = "\u200F" # Right-to-Left Mark
|
||||||
|
LRE = "\u202A" # Left-to-Right Embedding
|
||||||
|
PDF = "\u202C" # Pop Directional Formatting
|
||||||
|
|
||||||
|
_HEB_RE = re.compile(r'[\u0590-\u05FF]')
|
||||||
|
|
||||||
|
|
||||||
|
def _has_hebrew(text: str) -> bool:
|
||||||
|
return bool(_HEB_RE.search(text))
|
||||||
|
|
||||||
|
|
||||||
def bidi_table(headers: list[str], rows: list[list[str]]) -> str:
|
def bidi_table(headers: list[str], rows: list[list[str]]) -> str:
|
||||||
"""Render a box-drawing table safe for mixed RTL/LTR terminal display."""
|
"""Render a box-drawing table safe for mixed RTL/LTR terminal display."""
|
||||||
ncols = len(headers)
|
ncols = len(headers)
|
||||||
|
|
||||||
# Calculate column widths
|
# Calculate column widths (visual length, not counting bidi marks)
|
||||||
col_widths = [len(h) for h in headers]
|
col_widths = [len(h) for h in headers]
|
||||||
for row in rows:
|
for row in rows:
|
||||||
for i, cell in enumerate(row[:ncols]):
|
for i, cell in enumerate(row[:ncols]):
|
||||||
@@ -35,8 +46,10 @@ def bidi_table(headers: list[str], rows: list[list[str]]) -> str:
|
|||||||
for i in range(ncols):
|
for i in range(ncols):
|
||||||
cell = cells[i] if i < len(cells) else ""
|
cell = cells[i] if i < len(cells) else ""
|
||||||
padded = cell + " " * max(0, col_widths[i] - len(cell))
|
padded = cell + " " * max(0, col_widths[i] - len(cell))
|
||||||
parts.append(" " + padded + " ")
|
# Wrap each cell: LRE forces left-to-right context for the cell,
|
||||||
return LRM + "│" + (LRM + "│").join(parts) + LRM + "│"
|
# so box-drawing chars stay in place. PDF closes the embedding.
|
||||||
|
parts.append(LRE + " " + padded + " " + PDF)
|
||||||
|
return LRM + "│" + ("│").join(parts) + "│"
|
||||||
|
|
||||||
lines = [hline("┌", "┬", "┐")]
|
lines = [hline("┌", "┬", "┐")]
|
||||||
lines.append(dataline(headers))
|
lines.append(dataline(headers))
|
||||||
|
|||||||
@@ -84,6 +84,9 @@ async def create_project(
|
|||||||
# Link issue to legal-ai case via plugin state
|
# Link issue to legal-ai case via plugin state
|
||||||
await _link_case_to_issue(conn, issue_id, case_number)
|
await _link_case_to_issue(conn, issue_id, case_number)
|
||||||
|
|
||||||
|
# Verify project creation and close the setup issue
|
||||||
|
await _verify_and_close_setup_issue(conn, project_id, issue_id, identifier, case_number)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"id": project_id,
|
"id": project_id,
|
||||||
"company_id": company_id,
|
"company_id": company_id,
|
||||||
@@ -140,6 +143,70 @@ async def _link_case_to_issue(conn: asyncpg.Connection, issue_id: str, case_numb
|
|||||||
logger.info("Linked issue %s to case %s via plugin state", issue_id, case_number)
|
logger.info("Linked issue %s to case %s via plugin state", issue_id, case_number)
|
||||||
|
|
||||||
|
|
||||||
|
async def _verify_and_close_setup_issue(
|
||||||
|
conn: asyncpg.Connection,
|
||||||
|
project_id: str,
|
||||||
|
issue_id: str,
|
||||||
|
identifier: str,
|
||||||
|
case_number: str,
|
||||||
|
) -> None:
|
||||||
|
"""Verify the project was created correctly, then transition the setup issue to done."""
|
||||||
|
# Move to in_progress while verifying
|
||||||
|
await conn.execute(
|
||||||
|
"UPDATE issues SET status = 'in_progress', started_at = now() WHERE id = $1",
|
||||||
|
issue_id,
|
||||||
|
)
|
||||||
|
logger.info("%s: בביצוע — מאמת יצירת פרויקט", identifier)
|
||||||
|
|
||||||
|
# Verify: project exists, issue is linked, plugin state exists
|
||||||
|
checks = []
|
||||||
|
|
||||||
|
project = await conn.fetchrow("SELECT id, name FROM projects WHERE id = $1::uuid", project_id)
|
||||||
|
checks.append(("פרויקט נוצר", project is not None))
|
||||||
|
|
||||||
|
issue = await conn.fetchrow(
|
||||||
|
"SELECT id, project_id FROM issues WHERE id = $1 AND project_id = $2::uuid",
|
||||||
|
issue_id, project_id,
|
||||||
|
)
|
||||||
|
checks.append(("משימה משויכת לפרויקט", issue is not None))
|
||||||
|
|
||||||
|
plugin_link = await conn.fetchrow(
|
||||||
|
"SELECT value_json FROM plugin_state WHERE scope_id = $1 AND state_key = 'legal-case-number'",
|
||||||
|
issue_id,
|
||||||
|
)
|
||||||
|
checks.append(("קישור למערכת המשפטית", plugin_link is not None))
|
||||||
|
|
||||||
|
all_ok = all(ok for _, ok in checks)
|
||||||
|
report_lines = [f"{'✓' if ok else '✕'} {name}" for name, ok in checks]
|
||||||
|
report = "\n".join(report_lines)
|
||||||
|
|
||||||
|
if all_ok:
|
||||||
|
await conn.execute(
|
||||||
|
"UPDATE issues SET status = 'done', completed_at = now() WHERE id = $1",
|
||||||
|
issue_id,
|
||||||
|
)
|
||||||
|
# Document the verification in a comment
|
||||||
|
await conn.execute(
|
||||||
|
"""INSERT INTO issue_comments (id, company_id, issue_id, body)
|
||||||
|
VALUES ($1, (SELECT company_id FROM issues WHERE id = $2), $2,
|
||||||
|
$3)""",
|
||||||
|
str(uuid.uuid4()), issue_id,
|
||||||
|
f"## אימות יצירת פרויקט — ערר {case_number}\n\n{report}\n\nהפרויקט נוצר בהצלחה. משימה נסגרה אוטומטית.",
|
||||||
|
)
|
||||||
|
logger.info("%s: הושלם — פרויקט אומת ונסגר", identifier)
|
||||||
|
else:
|
||||||
|
# Leave in_progress with a warning comment
|
||||||
|
failed = [name for name, ok in checks if not ok]
|
||||||
|
await conn.execute(
|
||||||
|
"""INSERT INTO issue_comments (id, company_id, issue_id, body)
|
||||||
|
VALUES ($1, (SELECT company_id FROM issues WHERE id = $2), $2,
|
||||||
|
$3)""",
|
||||||
|
str(uuid.uuid4()), issue_id,
|
||||||
|
f"## אימות יצירת פרויקט — ערר {case_number}\n\n{report}\n\n⚠️ בדיקות שנכשלו: {', '.join(failed)}",
|
||||||
|
)
|
||||||
|
logger.warning("%s: אימות נכשל — %s", identifier, ", ".join(failed))
|
||||||
|
|
||||||
|
|
||||||
async def get_project_url(case_number: str) -> str | None:
|
async def get_project_url(case_number: str) -> str | None:
|
||||||
"""Find existing Paperclip project for a case number."""
|
"""Find existing Paperclip project for a case number."""
|
||||||
conn = await asyncpg.connect(PAPERCLIP_DB_URL)
|
conn = await asyncpg.connect(PAPERCLIP_DB_URL)
|
||||||
|
|||||||
Reference in New Issue
Block a user