fix: list_documents crashes when context.documents is a list

When EspoCRM returns documents as a flat list (legacy shape) instead of
the expected {totalFiles, folders, ...} dict, list_documents crashed with
"list object has no attribute 'get'". Now handles both shapes gracefully.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-19 11:00:23 +00:00
parent d91eb83db5
commit 5086fd48b8
+117 -4
View File
@@ -19,12 +19,83 @@ def register_document_tools(
):
"""Register document-related tools."""
async def list_documents() -> str:
async def list_documents(refresh: bool = False) -> str:
docs = context.get("documents", {})
if refresh and case_id:
try:
docs = await crm.post("SmartAssistant/action/listDocuments", {"caseId": case_id})
context["documents"] = docs
except Exception as e:
return fail(f"שגיאה ברענון רשימת מסמכים: {e}")
# Handle legacy shape where documents is a flat list of files
if isinstance(docs, list):
if not docs:
return ok("לא נמצאו מסמכים בתיק.")
lines = [f"נמצאו {len(docs)} מסמכים:"]
for f in docs:
if isinstance(f, dict):
lines.append(f'📄 {f.get("name", "?")}{f.get("path", "?")}')
else:
lines.append(f'📄 {f}')
return ok("\n".join(lines))
if not docs.get("totalFiles"):
return ok("לא נמצאו מסמכים")
parts = ["לא נמצאו מסמכים בתיק."]
resolved = docs.get("resolvedPath")
source = docs.get("pathSource")
exists = docs.get("folderExists")
diag = docs.get("diagnostic")
if resolved:
parts.append(f"נתיב מחושב: '{resolved}' (מקור: {source or 'unknown'}, קיים: {exists}).")
else:
parts.append("לא ניתן לחשב נתיב תיקייה לתיק.")
if diag:
parts.append(f"סיבה: {diag}")
parts.append("הצעה: find_case_folder(query=<שם איש קשר או מספר תיק>) כדי לאתר את התיקייה, ואז set_case_folder_path(path=...) לשמור.")
return ok(" ".join(parts))
return ok(json.dumps(docs, ensure_ascii=False))
async def browse_folder(path: str) -> str:
try:
result = await crm.post("SmartAssistant/action/browseFolder", {"path": path})
entries = result.get("entries", [])
if not entries:
return ok(f'תיקייה "{path}" ריקה או לא נמצאה.')
lines = [f'תוכן "{path}" ({len(entries)} פריטים):']
for e in entries:
kind = "📁" if e.get("type") == "folder" else "📄"
size = f' ({e["size"]} bytes)' if e.get("size") else ""
lines.append(f'{kind} {e.get("name")}{e.get("path")}{size}')
return ok("\n".join(lines))
except Exception as e:
return fail(f"שגיאה בסריקת תיקייה: {e}")
async def find_case_folder(query: str, limit: int = 20) -> str:
try:
result = await crm.post("SmartAssistant/action/findCaseFolder", {"query": query, "limit": limit})
matches = result.get("matches", [])
if not matches:
return ok(f'לא נמצאו תיקיות התואמות "{query}" ב-root של האחסון.')
lines = [f'נמצאו {len(matches)} תיקיות התואמות "{query}":']
for m in matches:
lines.append(f'{m.get("name")}{m.get("path")} (רמה {m.get("level")}, score={m.get("score")})')
return ok("\n".join(lines))
except Exception as e:
return fail(f"שגיאה בחיפוש תיקייה: {e}")
async def set_case_folder_path(path: str) -> str:
if not case_id:
return fail("צריך להיות בתוך תיק כדי להגדיר נתיב תיקייה.")
try:
result = await crm.post("SmartAssistant/action/setCaseFolderPath", {"caseId": case_id, "path": path})
if not result.get("success"):
return fail(f"לא הצלחתי לשמור נתיב: {result}")
context["documents"] = {}
return ok(f'נתיב התיקייה של התיק עודכן ל-"{path}". הפעל list_documents(refresh=true) כדי לטעון את הרשימה.')
except Exception as e:
return fail(f"שגיאה בשמירת נתיב: {e}")
async def read_document(filePath: str) -> str:
try:
result = await crm.post("SmartAssistant/action/readDocument", {"filePath": filePath})
@@ -87,11 +158,53 @@ def register_document_tools(
# Register
tools["list_documents"] = {
"description": "List documents in the case folder. Shows folder structure with file names and paths.",
"parameters": {"type": "object", "properties": {}},
"description": "List documents in the case folder. Shows folder structure with file names and paths. When totalFiles is 0, returns a diagnostic explaining why (missing path, folder doesn't exist, etc.). Pass refresh=true to re-fetch from EspoCRM after the folder path changes.",
"parameters": {
"type": "object",
"properties": {
"refresh": {"type": "boolean", "description": "Re-query EspoCRM instead of using cached context (default false)"},
},
},
"handler": list_documents,
}
tools["browse_folder"] = {
"description": "List the immediate contents of an arbitrary folder path on network storage. Escape hatch when list_documents returns no results — e.g. browse_folder('30-שוחר תום') to see what's really there.",
"parameters": {
"type": "object",
"properties": {
"path": {"type": "string", "description": "Folder path relative to the storage root (e.g. '33-שוחר תום' or 'Documents/2026')"},
},
"required": ["path"],
},
"handler": browse_folder,
}
tools["find_case_folder"] = {
"description": "Search network-storage for folders whose name contains the query (e.g. contact name or case number). Scans root + one level. Returns ranked matches. Use when list_documents shows the wrong or missing path.",
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "Substring to match (Hebrew or English; case-insensitive)."},
"limit": {"type": "integer", "description": "Max matches to return (default 20, max 50)"},
},
"required": ["query"],
},
"handler": find_case_folder,
}
tools["set_case_folder_path"] = {
"description": "Persist the given path as the case's networkStorageFolderPath. After this, list_documents(refresh=true) will read from the new path. Use only after find_case_folder or browse_folder confirms the path exists and matches this case.",
"parameters": {
"type": "object",
"properties": {
"path": {"type": "string", "description": "Storage-relative folder path confirmed to exist."},
},
"required": ["path"],
},
"handler": set_case_folder_path,
}
tools["read_document"] = {
"description": "Read and extract text content from a document (PDF, DOCX, TXT). Requires the file path from list_documents.",
"parameters": {