feat: improved ae_journal_search + AE integration docs

Search improvements:
- Switched from LIKE on default_qry_str to query_string path (fulltext
  MATCH/AGAINST IN BOOLEAN MODE — uses the index, supports +/- boolean ops)
- Added tag filter (icontains on tags field)
- Added date_from / date_to filters (created_on gte/lte)
- Added type_code / topic_code exact-match filters
- Added sort_by / sort_order control (updated, created, name, priority)
- Added status / priority filters
- Added page parameter for pagination
- Richer output: updated date, tags, pagination hint
- Updated Gemini tool declaration with all new params

Docs:
- documentation/ARCH__AE_INTEGRATION.md — journal_entry full schema,
  search operator reference, current tool inventory, planned phases
  (broader AE integration: tasks, people, calendar, knowledge import)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Idem
2026-04-30 20:10:04 -04:00
parent 77327d97ad
commit 71e472bebe
3 changed files with 367 additions and 33 deletions

View File

@@ -94,8 +94,9 @@ _ae_journal_list_declaration = types.FunctionDeclaration(
_ae_journal_search_declaration = types.FunctionDeclaration(
name="ae_journal_search",
description=(
"Search the Aether Journals knowledge base by keyword. "
"Use this to look up notes, documentation, meeting summaries, or any saved knowledge. "
"Search Aether Journal entries. All parameters are optional — combine freely. "
"Use 'query' for fulltext keyword search (supports boolean: +required -excluded \"phrase\"). "
"Use 'tags' to filter by tag substring. Use 'date_from'/'date_to' for date ranges (YYYY-MM-DD). "
"Always search before creating a new entry to avoid duplicates."
),
parameters=types.Schema(
@@ -103,21 +104,58 @@ _ae_journal_search_declaration = types.FunctionDeclaration(
properties={
"query": types.Schema(
type=types.Type.STRING,
description="Keyword or phrase to search for",
description="Fulltext keyword search. Supports boolean mode: +required -excluded \"exact phrase\".",
),
"journal_id": types.Schema(
type=types.Type.STRING,
description=(
"Optional: scope search to a specific journal by its id_random. "
"Omit to search all journals."
),
description="Scope results to a specific journal by its id_random. Omit to search all journals.",
),
"tags": types.Schema(
type=types.Type.STRING,
description="Filter by tag substring (e.g. 'networking' matches entries tagged 'networking' or 'home-networking').",
),
"type_code": types.Schema(
type=types.Type.STRING,
description="Filter by exact type_code (e.g. 'note', 'meeting', 'log').",
),
"topic_code": types.Schema(
type=types.Type.STRING,
description="Filter by exact topic_code.",
),
"date_from": types.Schema(
type=types.Type.STRING,
description="Return entries created on or after this date (YYYY-MM-DD).",
),
"date_to": types.Schema(
type=types.Type.STRING,
description="Return entries created on or before this date (YYYY-MM-DD).",
),
"sort_by": types.Schema(
type=types.Type.STRING,
description="Sort field: 'updated' (default), 'created', 'name', or 'priority'.",
),
"sort_order": types.Schema(
type=types.Type.STRING,
description="Sort direction: 'desc' (default, newest first) or 'asc'.",
),
"status": types.Schema(
type=types.Type.INTEGER,
description="Filter by exact status code.",
),
"priority": types.Schema(
type=types.Type.INTEGER,
description="Filter by exact priority (1=low, 5=high).",
),
"max_results": types.Schema(
type=types.Type.INTEGER,
description="Maximum number of entries to return (default 10)",
description="Number of results per page (default 10).",
),
"page": types.Schema(
type=types.Type.INTEGER,
description="Page number for pagination (default 1).",
),
},
required=["query"],
required=[],
),
)

View File

@@ -41,36 +41,95 @@ def _check_config() -> str | None:
# Tool: ae_journal_search
# ---------------------------------------------------------------------------
async def journal_search(query: str, journal_id: str | None = None, max_results: int = 10) -> str:
"""Search AE Journal entries by keyword.
async def journal_search(
query: str = "",
journal_id: str = "",
tags: str = "",
type_code: str = "",
topic_code: str = "",
date_from: str = "",
date_to: str = "",
sort_by: str = "updated",
sort_order: str = "desc",
status: int | None = None,
priority: int | None = None,
max_results: int = 10,
page: int = 1,
) -> str:
"""Search AE Journal entries.
Searches across the default_qry_str field (title + content excerpt).
Optionally scoped to a specific journal by journal_id (id_random).
Returns a markdown-formatted list of matching entries.
At least one of query, tags, type_code, topic_code, date_from, or journal_id
should be provided. All filters combine with AND.
"""
err = _check_config()
if err:
return err
return await asyncio.to_thread(_sync_journal_search, query, journal_id, max_results)
return await asyncio.to_thread(
_sync_journal_search,
query, journal_id, tags, type_code, topic_code,
date_from, date_to, sort_by, sort_order,
status, priority, max_results, page,
)
def _sync_journal_search(query: str, journal_id: str | None, max_results: int) -> str:
def _sync_journal_search(
query: str,
journal_id: str,
tags: str,
type_code: str,
topic_code: str,
date_from: str,
date_to: str,
sort_by: str,
sort_order: str,
status: int | None,
priority: int | None,
max_results: int,
page: int,
) -> str:
import requests
url = f"{settings.ae_api_url}/v3/crud/journal_entry/search"
search_body = {
"and_filters": [
{"field": "default_qry_str", "op": "icontains", "value": query}
],
"page_size": max_results,
# Build sort field
sort_field_map = {
"updated": "updated_on",
"created": "created_on",
"name": "name",
"priority": "priority",
}
sort_field = sort_field_map.get(sort_by, "updated_on")
order_by = f"{'-' if sort_order == 'desc' else ''}{sort_field}"
params = {}
search_body: dict = {"page_size": max_results, "page": page, "order_by": order_by}
# Fulltext keyword — uses MATCH/AGAINST index
if query:
search_body["query_string"] = query
# Additional AND filters
and_filters: list[dict] = []
if tags:
and_filters.append({"field": "tags", "op": "icontains", "value": tags})
if type_code:
and_filters.append({"field": "type_code", "op": "eq", "value": type_code})
if topic_code:
and_filters.append({"field": "topic_code", "op": "eq", "value": topic_code})
if date_from:
and_filters.append({"field": "created_on", "op": "gte", "value": date_from})
if date_to:
and_filters.append({"field": "created_on", "op": "lte", "value": date_to})
if status is not None:
and_filters.append({"field": "status", "op": "eq", "value": status})
if priority is not None:
and_filters.append({"field": "priority", "op": "eq", "value": priority})
if and_filters:
search_body["and_filters"] = and_filters
params: dict = {}
if journal_id:
params["for_obj_type"] = "journal"
params["for_obj_id"] = journal_id
url = f"{settings.ae_api_url}/v3/crud/journal_entry/search"
try:
resp = requests.post(
url,
@@ -86,17 +145,23 @@ def _sync_journal_search(query: str, journal_id: str | None, max_results: int) -
return f"Journal search error: {e}"
entries = data.get("data", [])
if not entries:
return f"No journal entries found matching: {query}"
total = data.get("total") or data.get("count") or len(entries)
if not entries:
desc = query or tags or type_code or topic_code or f"journal {journal_id}"
return f"No journal entries found for: {desc}"
label = query or tags or f"{len(entries)} entries"
lines = [f"Journal entries — **{label}** ({total} total, page {page}):\n"]
lines = [f"Journal entries matching **{query}** ({len(entries)} result(s)):\n"]
for entry in entries:
title = entry.get("name") or "(untitled)"
entry_id = entry.get("id_random", "")
journal_name = entry.get("journal_name") or entry.get("parent_name") or ""
summary = entry.get("summary") or ""
tags = entry.get("tags") or []
updated = (entry.get("updated_at") or entry.get("created_at") or "")[:10]
entry_tags = entry.get("tags") or []
updated = (entry.get("updated_on") or entry.get("updated_at") or
entry.get("created_on") or entry.get("created_at") or "")[:10]
content_preview = (entry.get("content") or "")[:400].replace("\n", " ")
header = f"**{title}**"
@@ -106,14 +171,18 @@ def _sync_journal_search(query: str, journal_id: str | None, max_results: int) -
if updated:
header += f" [{updated}]"
lines.append(header)
if tags:
lines.append(f" Tags: {', '.join(tags)}")
if entry_tags:
tag_list = entry_tags if isinstance(entry_tags, list) else [t.strip() for t in str(entry_tags).split(",")]
lines.append(f" Tags: {', '.join(tag_list)}")
if summary:
lines.append(f" Summary: {summary}")
lines.append(f" {summary}")
elif content_preview:
lines.append(f" {content_preview}{'' if len(entry.get('content','')) > 400 else ''}")
lines.append(f" {content_preview}{'' if len(entry.get('content', '')) > 400 else ''}")
lines.append("")
if total > page * max_results:
lines.append(f"(More results — call again with page={page + 1})")
return "\n".join(lines).strip()