tool improvment

This commit is contained in:
serversdwn
2025-12-31 22:36:24 -05:00
parent 6716245a99
commit b700ac3808
10 changed files with 598 additions and 80 deletions

View File

@@ -1,20 +1,42 @@
"""
Trilium notes executor for searching and creating notes via ETAPI.
This module provides integration with Trilium notes through the ETAPI HTTP API.
This module provides integration with Trilium notes through the ETAPI HTTP API
with improved resilience: timeout configuration, retry logic, and connection pooling.
"""
import os
import asyncio
import aiohttp
from typing import Dict
from typing import Dict, Optional
from ..utils.resilience import async_retry
TRILIUM_URL = os.getenv("TRILIUM_URL", "http://localhost:8080")
TRILIUM_TOKEN = os.getenv("TRILIUM_ETAPI_TOKEN", "")
# Module-level session for connection pooling
_session: Optional[aiohttp.ClientSession] = None
def get_session() -> aiohttp.ClientSession:
"""Get or create shared aiohttp session for connection pooling."""
global _session
if _session is None or _session.closed:
timeout = aiohttp.ClientTimeout(
total=float(os.getenv("TRILIUM_TIMEOUT", "30.0")),
connect=float(os.getenv("TRILIUM_CONNECT_TIMEOUT", "10.0"))
)
_session = aiohttp.ClientSession(timeout=timeout)
return _session
@async_retry(
max_attempts=3,
exceptions=(aiohttp.ClientError, asyncio.TimeoutError)
)
async def search_notes(args: Dict) -> Dict:
"""Search Trilium notes via ETAPI.
"""Search Trilium notes via ETAPI with retry logic.
Args:
args: Dictionary containing:
@@ -36,40 +58,72 @@ async def search_notes(args: Dict) -> Dict:
return {"error": "No query provided"}
if not TRILIUM_TOKEN:
return {"error": "TRILIUM_ETAPI_TOKEN not configured in environment"}
return {
"error": "TRILIUM_ETAPI_TOKEN not configured in environment",
"hint": "Set TRILIUM_ETAPI_TOKEN in .env file"
}
# Cap limit
limit = min(max(limit, 1), 20)
try:
async with aiohttp.ClientSession() as session:
async with session.get(
f"{TRILIUM_URL}/etapi/notes",
params={"search": query, "limit": limit},
headers={"Authorization": TRILIUM_TOKEN}
) as resp:
if resp.status == 200:
data = await resp.json()
# ETAPI returns {"results": [...]} format
results = data.get("results", [])
return {
"notes": results,
"count": len(results)
}
elif resp.status == 401:
return {"error": "Authentication failed. Check TRILIUM_ETAPI_TOKEN"}
else:
error_text = await resp.text()
return {"error": f"HTTP {resp.status}: {error_text}"}
session = get_session()
async with session.get(
f"{TRILIUM_URL}/etapi/notes",
params={"search": query, "limit": limit},
headers={"Authorization": TRILIUM_TOKEN}
) as resp:
if resp.status == 200:
data = await resp.json()
# ETAPI returns {"results": [...]} format
results = data.get("results", [])
return {
"notes": results,
"count": len(results)
}
elif resp.status == 401:
return {
"error": "Authentication failed. Check TRILIUM_ETAPI_TOKEN",
"status": 401
}
elif resp.status == 404:
return {
"error": "Trilium API endpoint not found. Check TRILIUM_URL",
"status": 404,
"url": TRILIUM_URL
}
else:
error_text = await resp.text()
return {
"error": f"HTTP {resp.status}: {error_text}",
"status": resp.status
}
except aiohttp.ClientConnectorError:
return {"error": f"Cannot connect to Trilium at {TRILIUM_URL}"}
except aiohttp.ClientConnectorError as e:
return {
"error": f"Cannot connect to Trilium at {TRILIUM_URL}",
"hint": "Check if Trilium is running and URL is correct",
"details": str(e)
}
except asyncio.TimeoutError:
timeout = os.getenv("TRILIUM_TIMEOUT", "30.0")
return {
"error": f"Trilium request timeout after {timeout}s",
"hint": "Trilium may be slow or unresponsive"
}
except Exception as e:
return {"error": f"Search failed: {str(e)}"}
return {
"error": f"Search failed: {str(e)}",
"type": type(e).__name__
}
@async_retry(
max_attempts=3,
exceptions=(aiohttp.ClientError, asyncio.TimeoutError)
)
async def create_note(args: Dict) -> Dict:
"""Create a note in Trilium via ETAPI.
"""Create a note in Trilium via ETAPI with retry logic.
Args:
args: Dictionary containing:
@@ -97,7 +151,10 @@ async def create_note(args: Dict) -> Dict:
return {"error": "No content provided"}
if not TRILIUM_TOKEN:
return {"error": "TRILIUM_ETAPI_TOKEN not configured in environment"}
return {
"error": "TRILIUM_ETAPI_TOKEN not configured in environment",
"hint": "Set TRILIUM_ETAPI_TOKEN in .env file"
}
# Prepare payload
payload = {
@@ -109,26 +166,51 @@ async def create_note(args: Dict) -> Dict:
}
try:
async with aiohttp.ClientSession() as session:
async with session.post(
f"{TRILIUM_URL}/etapi/create-note",
json=payload,
headers={"Authorization": TRILIUM_TOKEN}
) as resp:
if resp.status in [200, 201]:
data = await resp.json()
return {
"noteId": data.get("noteId"),
"title": title,
"success": True
}
elif resp.status == 401:
return {"error": "Authentication failed. Check TRILIUM_ETAPI_TOKEN"}
else:
error_text = await resp.text()
return {"error": f"HTTP {resp.status}: {error_text}"}
session = get_session()
async with session.post(
f"{TRILIUM_URL}/etapi/create-note",
json=payload,
headers={"Authorization": TRILIUM_TOKEN}
) as resp:
if resp.status in [200, 201]:
data = await resp.json()
return {
"noteId": data.get("noteId"),
"title": title,
"success": True
}
elif resp.status == 401:
return {
"error": "Authentication failed. Check TRILIUM_ETAPI_TOKEN",
"status": 401
}
elif resp.status == 404:
return {
"error": "Trilium API endpoint not found. Check TRILIUM_URL",
"status": 404,
"url": TRILIUM_URL
}
else:
error_text = await resp.text()
return {
"error": f"HTTP {resp.status}: {error_text}",
"status": resp.status
}
except aiohttp.ClientConnectorError:
return {"error": f"Cannot connect to Trilium at {TRILIUM_URL}"}
except aiohttp.ClientConnectorError as e:
return {
"error": f"Cannot connect to Trilium at {TRILIUM_URL}",
"hint": "Check if Trilium is running and URL is correct",
"details": str(e)
}
except asyncio.TimeoutError:
timeout = os.getenv("TRILIUM_TIMEOUT", "30.0")
return {
"error": f"Trilium request timeout after {timeout}s",
"hint": "Trilium may be slow or unresponsive"
}
except Exception as e:
return {"error": f"Note creation failed: {str(e)}"}
return {
"error": f"Note creation failed: {str(e)}",
"type": type(e).__name__
}