Merge pull request 'feat(mcp): FU-14 פרוסה 1 — get_appraiser_facts (GAP-44) + limit-caps (GAP-53)' (#62) from fix/fu14-slice1-appraiser-getter-limit-caps into main
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m50s
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m50s
This commit was merged in pull request #62.
This commit is contained in:
@@ -63,15 +63,15 @@ Kleppmann *DDIA* (idempotence) · IETF — *Idempotency-Key header* draft (https
|
|||||||
**כלל:** לכל כלי-חילוץ שכותב ל-DB יש **כלי-קריאה (get) מקביל**, והפלט **נשמר durably** (לא מוחזר-ונאבד).
|
**כלל:** לכל כלי-חילוץ שכותב ל-DB יש **כלי-קריאה (get) מקביל**, והפלט **נשמר durably** (לא מוחזר-ונאבד).
|
||||||
מופע של [G2](00-constitution.md#inv-g2-מקור-אמת-יחיד--אין-מסלולים-מקבילים-מתפצלים) (מקור-אמת נגיש). **פרויקטלי-תפעולי.**
|
מופע של [G2](00-constitution.md#inv-g2-מקור-אמת-יחיד--אין-מסלולים-מקבילים-מתפצלים) (מקור-אמת נגיש). **פרויקטלי-תפעולי.**
|
||||||
**מקור-סמכות:** דפוס `extract_claims`↔`get_claims`, `aggregate`↔`get_legal_arguments` ב-[server.py](../../mcp-server/src/legal_mcp/server.py).
|
**מקור-סמכות:** דפוס `extract_claims`↔`get_claims`, `aggregate`↔`get_legal_arguments` ב-[server.py](../../mcp-server/src/legal_mcp/server.py).
|
||||||
**אכיפה:** לכל extract — get מקביל. **כיום מופר:** `extract_appraiser_facts` כותב, **אין `get_appraiser_facts`** → חילוץ-חוזר יקר ולא-דטרמיניסטי.
|
**אכיפה:** לכל extract — get מקביל. **GAP-44 ✅ נסגר (2026-06-06):** נוסף `get_appraiser_facts` (קורא `list_appraiser_facts`+`detect_appraiser_conflicts`, ללא חילוץ-מחדש). נותר: תור-חילוץ סמוי (GAP-45).
|
||||||
**הפרה ידועה:** [gap-audit GAP-44](gap-audit.md); תור-חילוץ סמוי ([gap-audit GAP-45](gap-audit.md)).
|
**הפרה ידועה:** תור-חילוץ סמוי ([gap-audit GAP-45](gap-audit.md)).
|
||||||
|
|
||||||
### INV-TOOL5: limit-caps על כל כלי-רשימה/חיפוש
|
### INV-TOOL5: limit-caps על כל כלי-רשימה/חיפוש
|
||||||
**כלל:** לכל כלי שמחזיר רשימה יש **תקרת-limit נאכפת** (הגנה מפני עומס/DoS); pagination היכן שרלוונטי. **הנדסי.**
|
**כלל:** לכל כלי שמחזיר רשימה יש **תקרת-limit נאכפת** (הגנה מפני עומס/DoS); pagination היכן שרלוונטי. **הנדסי.**
|
||||||
**מקורות:** OWASP API Security Top 10 — *API4:2023 Unrestricted Resource Consumption* (https://owasp.org/API-Security/editions/2023/en/0xa4-unrestricted-resource-consumption/) ·
|
**מקורות:** OWASP API Security Top 10 — *API4:2023 Unrestricted Resource Consumption* (https://owasp.org/API-Security/editions/2023/en/0xa4-unrestricted-resource-consumption/) ·
|
||||||
Microsoft *REST API Guidelines* (pagination) · Stripe API (limit caps) | סטטוס: verified
|
Microsoft *REST API Guidelines* (pagination) · Stripe API (limit caps) | סטטוס: verified
|
||||||
**אכיפה:** clamp ל-max בכל כלי-רשימה. **כיום אין** — `precedent_library_list`/`search_*`/`missing_precedent_list` ללא תקרה; `list_chair_feedback` ללא limit כלל ([gap-audit GAP-53](gap-audit.md)).
|
**אכיפה:** clamp ל-max בכל כלי-רשימה. **GAP-53 ✅ נסגר (2026-06-06):** `_clamp_limit` (תקרה 200) על ~13 כלי list/search ב-[server.py](../../mcp-server/src/legal_mcp/server.py); `list_chair_feedback` קיבל param `limit` (server→workflow→db עם `LIMIT`).
|
||||||
**הפרה ידועה:** [gap-audit GAP-53](gap-audit.md).
|
**הפרה ידועה:** —
|
||||||
|
|
||||||
### INV-TOOL6: שלמות-הרשאות — כל כלי שהוראות-הסוכן דורשות מוענק
|
### INV-TOOL6: שלמות-הרשאות — כל כלי שהוראות-הסוכן דורשות מוענק
|
||||||
**כלל:** מפת-ההרשאות (אילו כלים מוענקים לכל סוכן) **תואמת** את מה שהוראות-הסוכן מצריכות — לא חסר ולא עודף.
|
**כלל:** מפת-ההרשאות (אילו כלים מוענקים לכל סוכן) **תואמת** את מה שהוראות-הסוכן מצריכות — לא חסר ולא עודף.
|
||||||
|
|||||||
@@ -198,6 +198,7 @@
|
|||||||
### FU-14 — חוזה כלי-ה-MCP
|
### FU-14 — חוזה כלי-ה-MCP
|
||||||
- **מכסה:** GAP-44,45,47..54 · **invariants:** INV-TOOL1–TOOL5 · **effort:** L · **תלויות:** FU-1
|
- **מכסה:** GAP-44,45,47..54 · **invariants:** INV-TOOL1–TOOL5 · **effort:** L · **תלויות:** FU-1
|
||||||
- **סוג:** code — envelope אחיד, מיזוג חיפוש/בלוקים, idempotency, limit-caps, get-symmetry, set_outcome SSoT
|
- **סוג:** code — envelope אחיד, מיזוג חיפוש/בלוקים, idempotency, limit-caps, get-symmetry, set_outcome SSoT
|
||||||
|
- **סטטוס חלקי (פרוסה 1, 2026-06-06):** ✅ **GAP-44** — נוסף `get_appraiser_facts` (ה-get המקביל ל-extract, INV-TOOL4); ✅ **GAP-53** — נוסף `_clamp_limit` (תקרה 200, INV-TOOL5) על ~13 כלי list/search + הוספת limit ל-`list_chair_feedback` (שהיה ללא תקרה). נותר: GAP-45 (status-tool), GAP-48 (envelope), GAP-49/50 (מיזוג+rename — שובר), GAP-51 (set_outcome enum SSoT), GAP-52 (idempotency).
|
||||||
|
|
||||||
### FU-15 — deploy/env/secrets
|
### FU-15 — deploy/env/secrets
|
||||||
- **מכסה:** GAP-55..62 · **invariants:** INV-ENV1–ENV5 · **effort:** M · **תלויות:** —
|
- **מכסה:** GAP-55..62 · **invariants:** INV-ENV1–ENV5 · **effort:** M · **תלויות:** —
|
||||||
|
|||||||
@@ -84,10 +84,24 @@ async def case_create(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# INV-TOOL5 / GAP-53: hard cap on list/search result sizes (OWASP API4:2023 —
|
||||||
|
# Unrestricted Resource Consumption). Non-positive is treated as "max", not "all".
|
||||||
|
_MAX_LIMIT = 200
|
||||||
|
|
||||||
|
|
||||||
|
def _clamp_limit(limit: int, hard_max: int = _MAX_LIMIT) -> int:
|
||||||
|
"""Clamp a caller-supplied result limit to [1, hard_max]."""
|
||||||
|
try:
|
||||||
|
n = int(limit)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return hard_max
|
||||||
|
return hard_max if n <= 0 else min(n, hard_max)
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
async def case_list(status: str = "", limit: int = 50) -> str:
|
async def case_list(status: str = "", limit: int = 50) -> str:
|
||||||
"""רשימת תיקי ערר. סינון אופציונלי לפי סטטוס (new/in_progress/drafted/reviewed/final)."""
|
"""רשימת תיקי ערר. סינון אופציונלי לפי סטטוס (new/in_progress/drafted/reviewed/final)."""
|
||||||
return await cases.case_list(status, limit)
|
return await cases.case_list(status, _clamp_limit(limit))
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
@@ -162,7 +176,7 @@ async def precedent_search_library(
|
|||||||
) -> str:
|
) -> str:
|
||||||
"""חיפוש בציטוטים שדפנה צירפה ידנית לתיקים בעבר (case_precedents).
|
"""חיפוש בציטוטים שדפנה צירפה ידנית לתיקים בעבר (case_precedents).
|
||||||
שונה מ-search_precedent_library שמחפש בקורפוס הפסיקה הסמכותית."""
|
שונה מ-search_precedent_library שמחפש בקורפוס הפסיקה הסמכותית."""
|
||||||
return await precedents.precedent_search_library(query, practice_area, limit)
|
return await precedents.precedent_search_library(query, practice_area, _clamp_limit(limit))
|
||||||
|
|
||||||
|
|
||||||
# ── External Precedent Library — authoritative case-law corpus ─────
|
# ── External Precedent Library — authoritative case-law corpus ─────
|
||||||
@@ -214,7 +228,7 @@ async def precedent_library_list(
|
|||||||
"""
|
"""
|
||||||
return await plib.precedent_library_list(
|
return await plib.precedent_library_list(
|
||||||
practice_area, court, precedent_level, source_type, search,
|
practice_area, court, precedent_level, source_type, search,
|
||||||
source_kind, limit,
|
source_kind, _clamp_limit(limit),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -273,7 +287,7 @@ async def style_corpus_enrich(corpus_id: str, overwrite: bool = False) -> str:
|
|||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
async def style_corpus_pending_enrichment(limit: int = 50) -> str:
|
async def style_corpus_pending_enrichment(limit: int = 50) -> str:
|
||||||
"""רשימת החלטות בקורפוס הסגנון שעדיין חסרות summary/outcome/key_principles — מועמדות לחילוץ."""
|
"""רשימת החלטות בקורפוס הסגנון שעדיין חסרות summary/outcome/key_principles — מועמדות לחילוץ."""
|
||||||
return await train_tools.list_corpus_pending_enrichment(limit)
|
return await train_tools.list_corpus_pending_enrichment(_clamp_limit(limit))
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
@@ -296,7 +310,7 @@ async def search_precedent_library(
|
|||||||
"""חיפוש סמנטי בקורפוס הפסיקה הסמכותית. מחזיר הלכות (מאושרות בלבד) + קטעי טקסט. השתמש כש-legal-writer צריך לצטט פסיקה מחייבת בבלוק י (CREAC: rule + explanation)."""
|
"""חיפוש סמנטי בקורפוס הפסיקה הסמכותית. מחזיר הלכות (מאושרות בלבד) + קטעי טקסט. השתמש כש-legal-writer צריך לצטט פסיקה מחייבת בבלוק י (CREAC: rule + explanation)."""
|
||||||
return await plib.search_precedent_library(
|
return await plib.search_precedent_library(
|
||||||
query, practice_area, court, precedent_level, appeal_subtype,
|
query, practice_area, court, precedent_level, appeal_subtype,
|
||||||
None, subject_tag, limit, include_halachot,
|
None, subject_tag, _clamp_limit(limit), include_halachot,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -320,7 +334,7 @@ async def halacha_review(
|
|||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
async def halachot_pending(limit: int = 100) -> str:
|
async def halachot_pending(limit: int = 100) -> str:
|
||||||
"""תור ההלכות הממתינות לאישור."""
|
"""תור ההלכות הממתינות לאישור."""
|
||||||
return await plib.halachot_pending(limit)
|
return await plib.halachot_pending(_clamp_limit(limit))
|
||||||
|
|
||||||
|
|
||||||
# Documents
|
# Documents
|
||||||
@@ -439,7 +453,7 @@ async def search_decisions(
|
|||||||
) -> str:
|
) -> str:
|
||||||
"""חיפוש סמנטי בהחלטות קודמות ובמסמכים — מסונן לפי תחום משפטי."""
|
"""חיפוש סמנטי בהחלטות קודמות ובמסמכים — מסונן לפי תחום משפטי."""
|
||||||
return await search.search_decisions(
|
return await search.search_decisions(
|
||||||
query, limit, section_type, practice_area, appeal_subtype, case_number,
|
query, _clamp_limit(limit), section_type, practice_area, appeal_subtype, case_number,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -450,7 +464,7 @@ async def search_case_documents(
|
|||||||
limit: int = 10,
|
limit: int = 10,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""חיפוש סמנטי בתוך מסמכי תיק ספציפי."""
|
"""חיפוש סמנטי בתוך מסמכי תיק ספציפי."""
|
||||||
return await search.search_case_documents(case_number, query, limit)
|
return await search.search_case_documents(case_number, query, _clamp_limit(limit))
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
@@ -463,7 +477,7 @@ async def find_similar_cases(
|
|||||||
) -> str:
|
) -> str:
|
||||||
"""מציאת תיקים דומים על בסיס תיאור — מסונן לפי תחום משפטי."""
|
"""מציאת תיקים דומים על בסיס תיאור — מסונן לפי תחום משפטי."""
|
||||||
return await search.find_similar_cases(
|
return await search.find_similar_cases(
|
||||||
description, limit, practice_area, appeal_subtype, case_number,
|
description, _clamp_limit(limit), practice_area, appeal_subtype, case_number,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -496,7 +510,7 @@ async def search_internal_decisions(
|
|||||||
כשרוצים להרחיב מעבר לטקסט המקורי. default False.
|
כשרוצים להרחיב מעבר לטקסט המקורי. default False.
|
||||||
"""
|
"""
|
||||||
return await search.search_internal_decisions(
|
return await search.search_internal_decisions(
|
||||||
query, practice_area, appeal_subtype, district, chair_name, limit, include_halachot,
|
query, practice_area, appeal_subtype, district, chair_name, _clamp_limit(limit), include_halachot,
|
||||||
include_cited_by=include_cited_by,
|
include_cited_by=include_cited_by,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -571,6 +585,12 @@ async def extract_appraiser_facts(case_number: str) -> str:
|
|||||||
return await drafting.extract_appraiser_facts(case_number)
|
return await drafting.extract_appraiser_facts(case_number)
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def get_appraiser_facts(case_number: str) -> str:
|
||||||
|
"""קריאת עובדות-השמאי שכבר חולצו (facts + סתירות) — ללא חילוץ-מחדש יקר. ה-get המקביל ל-extract_appraiser_facts."""
|
||||||
|
return await drafting.get_appraiser_facts(case_number)
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
async def write_interim_draft(case_number: str, instructions: str = "") -> str:
|
async def write_interim_draft(case_number: str, instructions: str = "") -> str:
|
||||||
"""כתיבת ארבעת הבלוקים לטיוטת ביניים (רקע, תכניות+היתרים, טענות, הליכים) — אותו skill וטמפלט."""
|
"""כתיבת ארבעת הבלוקים לטיוטת ביניים (רקע, תכניות+היתרים, טענות, הליכים) — אותו skill וטמפלט."""
|
||||||
@@ -813,7 +833,7 @@ async def missing_precedent_list(
|
|||||||
case_number=case_number,
|
case_number=case_number,
|
||||||
status=status,
|
status=status,
|
||||||
legal_topic=legal_topic,
|
legal_topic=legal_topic,
|
||||||
limit=limit,
|
limit=_clamp_limit(limit),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -878,7 +898,7 @@ async def list_internal_citations(
|
|||||||
return await cit_tools.list_internal_citations(
|
return await cit_tools.list_internal_citations(
|
||||||
case_law_id=case_law_id,
|
case_law_id=case_law_id,
|
||||||
linked_only=linked_only,
|
linked_only=linked_only,
|
||||||
limit=limit,
|
limit=_clamp_limit(limit),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -894,7 +914,7 @@ async def list_incoming_citations(
|
|||||||
"""
|
"""
|
||||||
return await cit_tools.list_incoming_citations(
|
return await cit_tools.list_incoming_citations(
|
||||||
case_law_id=case_law_id,
|
case_law_id=case_law_id,
|
||||||
limit=limit,
|
limit=_clamp_limit(limit),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -917,9 +937,10 @@ async def list_chair_feedback(
|
|||||||
case_number: str = "",
|
case_number: str = "",
|
||||||
category: str = "",
|
category: str = "",
|
||||||
unresolved_only: bool = True,
|
unresolved_only: bool = True,
|
||||||
|
limit: int = 100,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""הצגת הערות יו"ר שתועדו — אפשר לסנן לפי תיק, קטגוריה, מטופלות."""
|
"""הצגת הערות יו"ר שתועדו — אפשר לסנן לפי תיק, קטגוריה, מטופלות."""
|
||||||
return await workflow.list_chair_feedback(case_number, category, unresolved_only)
|
return await workflow.list_chair_feedback(case_number, category, unresolved_only, _clamp_limit(limit))
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
|
|||||||
@@ -2428,8 +2428,9 @@ async def list_chair_feedback(
|
|||||||
case_id: UUID | None = None,
|
case_id: UUID | None = None,
|
||||||
category: str | None = None,
|
category: str | None = None,
|
||||||
unresolved_only: bool = False,
|
unresolved_only: bool = False,
|
||||||
|
limit: int = 100,
|
||||||
) -> list[dict]:
|
) -> list[dict]:
|
||||||
"""List chair feedback, optionally filtered."""
|
"""List chair feedback, optionally filtered. Capped by limit (INV-TOOL5 / GAP-53)."""
|
||||||
pool = await get_pool()
|
pool = await get_pool()
|
||||||
conditions = []
|
conditions = []
|
||||||
params: list = []
|
params: list = []
|
||||||
@@ -2447,9 +2448,10 @@ async def list_chair_feedback(
|
|||||||
conditions.append("resolved = FALSE")
|
conditions.append("resolved = FALSE")
|
||||||
|
|
||||||
where = f"WHERE {' AND '.join(conditions)}" if conditions else ""
|
where = f"WHERE {' AND '.join(conditions)}" if conditions else ""
|
||||||
|
params.append(max(1, int(limit)))
|
||||||
async with pool.acquire() as conn:
|
async with pool.acquire() as conn:
|
||||||
rows = await conn.fetch(
|
rows = await conn.fetch(
|
||||||
f"SELECT * FROM chair_feedback {where} ORDER BY created_at DESC",
|
f"SELECT * FROM chair_feedback {where} ORDER BY created_at DESC LIMIT ${idx}",
|
||||||
*params,
|
*params,
|
||||||
)
|
)
|
||||||
return [dict(r) for r in rows]
|
return [dict(r) for r in rows]
|
||||||
|
|||||||
@@ -477,6 +477,37 @@ async def extract_appraiser_facts(case_number: str) -> str:
|
|||||||
ensure_ascii=False, indent=2)
|
ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_appraiser_facts(case_number: str) -> str:
|
||||||
|
"""קריאת עובדות-השמאי שכבר חולצו לתיק — ללא הרצת חילוץ מחדש (INV-TOOL4 / GAP-44).
|
||||||
|
|
||||||
|
ה-get המקביל ל-extract_appraiser_facts: מחזיר את העובדות השמורות בטבלת
|
||||||
|
appraiser_facts + סתירות מזוהות בין שמאים, בלי קריאת-LLM יקרה ולא-דטרמיניסטית.
|
||||||
|
מחזיר facts ריק אם החילוץ טרם רץ (status=ok, count=0) — לא שגיאה.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
case_number: מספר תיק הערר
|
||||||
|
"""
|
||||||
|
case = await db.get_case_by_number(case_number)
|
||||||
|
if not case:
|
||||||
|
return json.dumps({"status": "error",
|
||||||
|
"message": f"תיק {case_number} לא נמצא."},
|
||||||
|
ensure_ascii=False, indent=2)
|
||||||
|
case_id = UUID(case["id"])
|
||||||
|
try:
|
||||||
|
facts = await db.list_appraiser_facts(case_id)
|
||||||
|
conflicts = await db.detect_appraiser_conflicts(case_id)
|
||||||
|
return json.dumps({
|
||||||
|
"status": "ok",
|
||||||
|
"case_number": case_number,
|
||||||
|
"count": len(facts),
|
||||||
|
"facts": facts,
|
||||||
|
"conflicts": conflicts,
|
||||||
|
}, default=str, ensure_ascii=False, indent=2)
|
||||||
|
except Exception as e:
|
||||||
|
return json.dumps({"status": "error", "message": str(e)},
|
||||||
|
ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
|
||||||
async def write_interim_draft(case_number: str, instructions: str = "") -> str:
|
async def write_interim_draft(case_number: str, instructions: str = "") -> str:
|
||||||
"""כתיבת ארבעת הבלוקים לטיוטת ביניים: רקע (ו), תכניות+היתרים (ט),
|
"""כתיבת ארבעת הבלוקים לטיוטת ביניים: רקע (ו), תכניות+היתרים (ט),
|
||||||
טענות הצדדים (ז), הליכים (ח). אם לא חולצו עובדות שמאיות עדיין —
|
טענות הצדדים (ז), הליכים (ח). אם לא חולצו עובדות שמאיות עדיין —
|
||||||
|
|||||||
@@ -394,6 +394,7 @@ async def list_chair_feedback(
|
|||||||
case_number: str = "",
|
case_number: str = "",
|
||||||
category: str = "",
|
category: str = "",
|
||||||
unresolved_only: bool = True,
|
unresolved_only: bool = True,
|
||||||
|
limit: int = 100,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""הצגת הערות יו"ר שתועדו, עם אפשרות סינון.
|
"""הצגת הערות יו"ר שתועדו, עם אפשרות סינון.
|
||||||
|
|
||||||
@@ -401,6 +402,7 @@ async def list_chair_feedback(
|
|||||||
case_number: סינון לפי תיק (אם ריק — כל ההערות)
|
case_number: סינון לפי תיק (אם ריק — כל ההערות)
|
||||||
category: סינון לפי קטגוריה
|
category: סינון לפי קטגוריה
|
||||||
unresolved_only: האם להציג רק הערות שלא טופלו (ברירת מחדל: כן)
|
unresolved_only: האם להציג רק הערות שלא טופלו (ברירת מחדל: כן)
|
||||||
|
limit: תקרת תוצאות (INV-TOOL5 / GAP-53)
|
||||||
"""
|
"""
|
||||||
case_id = None
|
case_id = None
|
||||||
if case_number:
|
if case_number:
|
||||||
@@ -412,6 +414,7 @@ async def list_chair_feedback(
|
|||||||
case_id=case_id,
|
case_id=case_id,
|
||||||
category=category or None,
|
category=category or None,
|
||||||
unresolved_only=unresolved_only,
|
unresolved_only=unresolved_only,
|
||||||
|
limit=limit,
|
||||||
)
|
)
|
||||||
|
|
||||||
if not feedbacks:
|
if not feedbacks:
|
||||||
|
|||||||
Reference in New Issue
Block a user