refactor: split tool declarations into domain files + role config UI
tools/__init__.py shrinks from 1,137 → 250 lines. Each domain file now owns both its callables and its FunctionDeclarations (DECLARATIONS list), so adding a new tool only touches one file. New TOOL_CATEGORIES dict exported from __init__ — used by the UI for grouped tool checkboxes. Role config UI (Settings → Model Registry → Role Assignments): - ⚙ button per role expands an inline configure panel - Textarea for system_append (injected into system prompt for this role) - Grouped checkboxes for tool allow-list (all checked = no restriction) - POST /api/models/role-config saves both fields; updates ROLE_CONFIG_DATA in-page so re-open reflects current state without a page reload Backend: - model_registry.set_role_config() writes system_append + tools to registry - TOOL_CATEGORIES exported from tools/__init__ for UI rendering - TOOLS.md header updated: 30 → 39 tools (ae_journal_* and cortex_* additions) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -415,6 +415,24 @@ def get_best_local_model(username: str, role: str = "chat") -> dict | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def set_role_config(username: str, role: str, system_append: str, tools: list[str] | None) -> None:
|
||||||
|
"""Save system_append and tools allow-list for a role.
|
||||||
|
|
||||||
|
tools=None clears the allow-list (role uses all accessible tools).
|
||||||
|
tools=[] would mean no tools at all — validate in the caller if that's undesired.
|
||||||
|
"""
|
||||||
|
data = _load(username)
|
||||||
|
roles = data.setdefault("roles", {})
|
||||||
|
if role not in roles:
|
||||||
|
roles[role] = {}
|
||||||
|
roles[role]["system_append"] = system_append.strip()
|
||||||
|
if tools is None:
|
||||||
|
roles[role].pop("tools", None)
|
||||||
|
else:
|
||||||
|
roles[role]["tools"] = [t for t in tools if t]
|
||||||
|
_save(username, data)
|
||||||
|
|
||||||
|
|
||||||
def get_role_config(username: str, role: str) -> dict:
|
def get_role_config(username: str, role: str) -> dict:
|
||||||
"""
|
"""
|
||||||
Return supplemental config for a role: system_append and tools.
|
Return supplemental config for a role: system_append and tools.
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
|
|||||||
from auth_utils import COOKIE_NAME, decode_token
|
from auth_utils import COOKIE_NAME, decode_token
|
||||||
from config import settings as app_settings
|
from config import settings as app_settings
|
||||||
import model_registry as reg
|
import model_registry as reg
|
||||||
|
from tools import TOOL_CATEGORIES
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
@@ -285,13 +286,42 @@ def _render(username: str, success: str = "", error: str = "") -> str:
|
|||||||
f'data-slot="{slot}" title="{slot_label}">\n{model_opts}\n</select>'
|
f'data-slot="{slot}" title="{slot_label}">\n{model_opts}\n</select>'
|
||||||
)
|
)
|
||||||
role_rows += f'<div class="role-slot"><span class="slot-label">{slot_label}</span>{sel}</div>'
|
role_rows += f'<div class="role-slot"><span class="slot-label">{slot_label}</span>{sel}</div>'
|
||||||
role_rows += '</div></div>'
|
role_rows += (
|
||||||
|
f'</div>'
|
||||||
|
f'<button class="role-cfg-btn" data-role="{role}" title="Configure persona and tools">⚙</button>'
|
||||||
|
f'</div>'
|
||||||
|
f'<div class="role-config-panel" id="rcp-{role}">'
|
||||||
|
f'<div class="rcp-field">'
|
||||||
|
f'<label class="rcp-label">System prompt addition</label>'
|
||||||
|
f'<textarea class="rcp-textarea" data-role="{role}" rows="3" '
|
||||||
|
f'placeholder="Extra instructions injected into the system prompt when this role is active…"></textarea>'
|
||||||
|
f'</div>'
|
||||||
|
f'<div class="rcp-field">'
|
||||||
|
f'<label class="rcp-label">Tool allow-list '
|
||||||
|
f'<span class="rcp-hint">— all checked means no restriction (use all accessible tools)</span></label>'
|
||||||
|
f'<div class="rcp-tools" id="rcp-tools-{role}"></div>'
|
||||||
|
f'</div>'
|
||||||
|
f'<div class="rcp-actions">'
|
||||||
|
f'<button class="btn btn-primary btn-sm rcp-save" data-role="{role}">Save</button>'
|
||||||
|
f'<button class="btn btn-secondary btn-sm rcp-cancel" data-role="{role}">Cancel</button>'
|
||||||
|
f'</div>'
|
||||||
|
f'</div>'
|
||||||
|
)
|
||||||
|
|
||||||
role_data_js = _json.dumps({
|
role_data_js = _json.dumps({
|
||||||
role: {slot: (roles.get(role, {}).get(slot) or "") for slot in reg.PRIORITY_KEYS[:3]}
|
role: {slot: (roles.get(role, {}).get(slot) or "") for slot in reg.PRIORITY_KEYS[:3]}
|
||||||
for role in app_settings.get_defined_roles()
|
for role in app_settings.get_defined_roles()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
role_config_data_js = _json.dumps({
|
||||||
|
role: {
|
||||||
|
"system_append": roles.get(role, {}).get("system_append", ""),
|
||||||
|
"tools": roles.get(role, {}).get("tools") or None,
|
||||||
|
}
|
||||||
|
for role in app_settings.get_defined_roles()
|
||||||
|
})
|
||||||
|
tool_categories_js = _json.dumps(TOOL_CATEGORIES)
|
||||||
|
|
||||||
# ── Catalog data + Google accounts for JS ─────────────────────────────────
|
# ── Catalog data + Google accounts for JS ─────────────────────────────────
|
||||||
google_accounts_js = _json.dumps(reg.get_google_accounts(username))
|
google_accounts_js = _json.dumps(reg.get_google_accounts(username))
|
||||||
google_catalog_js = _json.dumps(reg.get_catalog("google"))
|
google_catalog_js = _json.dumps(reg.get_catalog("google"))
|
||||||
@@ -305,8 +335,10 @@ def _render(username: str, success: str = "", error: str = "") -> str:
|
|||||||
"{{ host_rows }}": host_rows,
|
"{{ host_rows }}": host_rows,
|
||||||
"{{ model_rows }}": model_rows,
|
"{{ model_rows }}": model_rows,
|
||||||
"{{ host_options }}": host_options,
|
"{{ host_options }}": host_options,
|
||||||
"{{ role_rows }}": role_rows,
|
"{{ role_rows }}": role_rows,
|
||||||
"{{ role_data_js }}": role_data_js,
|
"{{ role_data_js }}": role_data_js,
|
||||||
|
"{{ role_config_data_js }}": role_config_data_js,
|
||||||
|
"{{ tool_categories_js }}": tool_categories_js,
|
||||||
"{{ google_accounts_js }}": google_accounts_js,
|
"{{ google_accounts_js }}": google_accounts_js,
|
||||||
"{{ google_catalog_js }}": google_catalog_js,
|
"{{ google_catalog_js }}": google_catalog_js,
|
||||||
"{{ anthropic_catalog_js }}": anthropic_catalog_js,
|
"{{ anthropic_catalog_js }}": anthropic_catalog_js,
|
||||||
@@ -510,6 +542,36 @@ async def set_role(request: Request) -> JSONResponse:
|
|||||||
return JSONResponse({"ok": True})
|
return JSONResponse({"ok": True})
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/api/models/role-config")
|
||||||
|
async def set_role_config(request: Request) -> JSONResponse:
|
||||||
|
"""AJAX: save system_append and tool allow-list for a role.
|
||||||
|
|
||||||
|
Body: {"role": "coder", "system_append": "...", "tools": ["web_search", ...] | null}
|
||||||
|
tools=null clears the allow-list (role uses all accessible tools).
|
||||||
|
"""
|
||||||
|
username = _get_user(request)
|
||||||
|
if not username:
|
||||||
|
return JSONResponse({"error": "Not authenticated"}, status_code=401)
|
||||||
|
try:
|
||||||
|
body = await request.json()
|
||||||
|
except Exception:
|
||||||
|
return JSONResponse({"error": "Invalid JSON"}, status_code=400)
|
||||||
|
|
||||||
|
role = body.get("role", "").strip()
|
||||||
|
system_append = body.get("system_append", "")
|
||||||
|
tools = body.get("tools") # list[str] or None
|
||||||
|
|
||||||
|
if not role:
|
||||||
|
return JSONResponse({"error": "role is required"}, status_code=400)
|
||||||
|
if tools is not None and not isinstance(tools, list):
|
||||||
|
return JSONResponse({"error": "tools must be a list or null"}, status_code=400)
|
||||||
|
|
||||||
|
reg.set_role_config(username, role, system_append, tools)
|
||||||
|
logger.info("role config saved: %s %s (tools=%s)", username, role,
|
||||||
|
len(tools) if tools is not None else "all")
|
||||||
|
return JSONResponse({"ok": True})
|
||||||
|
|
||||||
|
|
||||||
@router.get("/api/local-llm/fetch-models")
|
@router.get("/api/local-llm/fetch-models")
|
||||||
async def fetch_models(request: Request, host_id: str = "") -> JSONResponse:
|
async def fetch_models(request: Request, host_id: str = "") -> JSONResponse:
|
||||||
"""Proxy to the host's models endpoint. host_id selects which host."""
|
"""Proxy to the host's models endpoint. host_id selects which host."""
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Tool Reference
|
# Tool Reference
|
||||||
|
|
||||||
> This reference covers all 30 orchestrator tools available when the ⚡ toggle is on.
|
> This reference covers all 39 orchestrator tools available when the ⚡ toggle is on.
|
||||||
> Tools are invoked automatically by the orchestrator — you don't call them directly.
|
> Tools are invoked automatically by the orchestrator — you don't call them directly.
|
||||||
|
|
||||||
¹ **Admin only** — requires the `admin` role. Invisible to regular users.
|
¹ **Admin only** — requires the `admin` role. Invisible to regular users.
|
||||||
|
|||||||
@@ -223,7 +223,6 @@
|
|||||||
display: flex; align-items: flex-start; gap: 1rem;
|
display: flex; align-items: flex-start; gap: 1rem;
|
||||||
padding: 0.6rem 0; border-bottom: 1px solid var(--pg-border-deep);
|
padding: 0.6rem 0; border-bottom: 1px solid var(--pg-border-deep);
|
||||||
}
|
}
|
||||||
.role-row:last-child { border-bottom: none; }
|
|
||||||
.role-name { font-size: 0.82rem; font-weight: 600; color: #a78bfa; min-width: 6rem; padding-top: 0.45rem; }
|
.role-name { font-size: 0.82rem; font-weight: 600; color: #a78bfa; min-width: 6rem; padding-top: 0.45rem; }
|
||||||
.role-slots { display: flex; flex-wrap: wrap; gap: 0.5rem; flex: 1; }
|
.role-slots { display: flex; flex-wrap: wrap; gap: 0.5rem; flex: 1; }
|
||||||
.role-slot { display: flex; flex-direction: column; gap: 0.2rem; flex: 1; min-width: 8rem; }
|
.role-slot { display: flex; flex-direction: column; gap: 0.2rem; flex: 1; min-width: 8rem; }
|
||||||
@@ -238,6 +237,36 @@
|
|||||||
.role-select.saved { border-color: #166534; }
|
.role-select.saved { border-color: #166534; }
|
||||||
.role-select.saving { border-color: #92400e; }
|
.role-select.saving { border-color: #92400e; }
|
||||||
.role-select.err { border-color: #7f1d1d; }
|
.role-select.err { border-color: #7f1d1d; }
|
||||||
|
.role-cfg-btn {
|
||||||
|
flex-shrink: 0; padding: 0.3rem 0.55rem; font-size: 0.8rem;
|
||||||
|
background: none; border: 1px solid var(--pg-border); border-radius: 6px;
|
||||||
|
color: var(--pg-dim); cursor: pointer; transition: color 0.15s, border-color 0.15s;
|
||||||
|
margin-top: 0.35rem;
|
||||||
|
}
|
||||||
|
.role-cfg-btn:hover { color: #a78bfa; border-color: #a78bfa; }
|
||||||
|
.role-cfg-btn.active { color: #a78bfa; border-color: #a78bfa; background: rgba(167,139,250,0.08); }
|
||||||
|
/* Role config panel */
|
||||||
|
.role-config-panel {
|
||||||
|
display: none; margin: 0 0 0.75rem 7rem;
|
||||||
|
border: 1px solid var(--pg-border); border-radius: 8px;
|
||||||
|
background: var(--pg-surface); padding: 1rem;
|
||||||
|
}
|
||||||
|
.role-config-panel.open { display: block; }
|
||||||
|
.rcp-field { margin-bottom: 0.75rem; }
|
||||||
|
.rcp-label { display: block; font-size: 0.75rem; font-weight: 600; color: var(--pg-muted); margin-bottom: 0.35rem; text-transform: uppercase; letter-spacing: 0.04em; }
|
||||||
|
.rcp-hint { font-weight: 400; text-transform: none; letter-spacing: 0; color: var(--pg-dimmer); }
|
||||||
|
.rcp-textarea {
|
||||||
|
width: 100%; resize: vertical; min-height: 4rem;
|
||||||
|
background: var(--pg-bg); border: 1px solid var(--pg-border); border-radius: 6px;
|
||||||
|
color: var(--pg-text); font-family: inherit; font-size: 0.85rem;
|
||||||
|
padding: 0.5rem 0.6rem; outline: none; transition: border-color 0.15s;
|
||||||
|
}
|
||||||
|
.rcp-textarea:focus { border-color: #7c3aed; }
|
||||||
|
.rcp-tools { display: flex; flex-wrap: wrap; gap: 0.4rem 1rem; }
|
||||||
|
.rcp-cat { width: 100%; margin: 0.4rem 0 0.1rem; font-size: 0.7rem; font-weight: 600; color: var(--pg-dimmer); text-transform: uppercase; letter-spacing: 0.05em; }
|
||||||
|
.rcp-check { display: flex; align-items: center; gap: 0.35rem; font-size: 0.8rem; color: var(--pg-bright); cursor: pointer; }
|
||||||
|
.rcp-check input { accent-color: #a78bfa; cursor: pointer; }
|
||||||
|
.rcp-actions { display: flex; gap: 0.5rem; padding-top: 0.25rem; }
|
||||||
|
|
||||||
/* Model select picker */
|
/* Model select picker */
|
||||||
#model-select-wrap { display: none; margin-bottom: 0.75rem; }
|
#model-select-wrap { display: none; margin-bottom: 0.75rem; }
|
||||||
@@ -496,6 +525,8 @@
|
|||||||
<script>
|
<script>
|
||||||
// ── Injected data ─────────────────────────────────────────────────────────
|
// ── Injected data ─────────────────────────────────────────────────────────
|
||||||
const ROLE_DATA = {{ role_data_js }};
|
const ROLE_DATA = {{ role_data_js }};
|
||||||
|
const ROLE_CONFIG_DATA = {{ role_config_data_js }};
|
||||||
|
const TOOL_CATEGORIES = {{ tool_categories_js }};
|
||||||
const GOOGLE_ACCOUNTS = {{ google_accounts_js }};
|
const GOOGLE_ACCOUNTS = {{ google_accounts_js }};
|
||||||
const GOOGLE_CATALOG = {{ google_catalog_js }};
|
const GOOGLE_CATALOG = {{ google_catalog_js }};
|
||||||
const ANTHROPIC_CATALOG = {{ anthropic_catalog_js }};
|
const ANTHROPIC_CATALOG = {{ anthropic_catalog_js }};
|
||||||
@@ -543,6 +574,112 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── Role config panels ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// All tool names in category order (for checkbox rendering)
|
||||||
|
const ALL_TOOLS_ORDERED = Object.entries(TOOL_CATEGORIES).flatMap(([,tools]) => tools);
|
||||||
|
|
||||||
|
function buildToolChecklist(role, savedTools) {
|
||||||
|
// savedTools: null = all, array = explicit allow-list
|
||||||
|
const wrap = document.getElementById(`rcp-tools-${role}`);
|
||||||
|
if (!wrap) return;
|
||||||
|
wrap.innerHTML = '';
|
||||||
|
for (const [cat, tools] of Object.entries(TOOL_CATEGORIES)) {
|
||||||
|
const catEl = document.createElement('div');
|
||||||
|
catEl.className = 'rcp-cat';
|
||||||
|
catEl.style.width = '100%';
|
||||||
|
catEl.textContent = cat;
|
||||||
|
wrap.appendChild(catEl);
|
||||||
|
for (const tool of tools) {
|
||||||
|
const label = document.createElement('label');
|
||||||
|
label.className = 'rcp-check';
|
||||||
|
const cb = document.createElement('input');
|
||||||
|
cb.type = 'checkbox';
|
||||||
|
cb.value = tool;
|
||||||
|
cb.checked = savedTools === null || savedTools.includes(tool);
|
||||||
|
label.appendChild(cb);
|
||||||
|
label.appendChild(document.createTextNode(tool));
|
||||||
|
wrap.appendChild(label);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openRolePanel(role) {
|
||||||
|
const panel = document.getElementById(`rcp-${role}`);
|
||||||
|
const btn = document.querySelector(`.role-cfg-btn[data-role="${role}"]`);
|
||||||
|
const cfg = ROLE_CONFIG_DATA[role] || {};
|
||||||
|
if (!panel) return;
|
||||||
|
// Populate textarea
|
||||||
|
panel.querySelector('.rcp-textarea').value = cfg.system_append || '';
|
||||||
|
// Build tool checklist
|
||||||
|
buildToolChecklist(role, cfg.tools || null);
|
||||||
|
panel.classList.add('open');
|
||||||
|
btn && btn.classList.add('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeRolePanel(role) {
|
||||||
|
const panel = document.getElementById(`rcp-${role}`);
|
||||||
|
const btn = document.querySelector(`.role-cfg-btn[data-role="${role}"]`);
|
||||||
|
panel && panel.classList.remove('open');
|
||||||
|
btn && btn.classList.remove('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelectorAll('.role-cfg-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
const role = btn.dataset.role;
|
||||||
|
const panel = document.getElementById(`rcp-${role}`);
|
||||||
|
if (panel.classList.contains('open')) {
|
||||||
|
closeRolePanel(role);
|
||||||
|
} else {
|
||||||
|
// Close any other open panels first
|
||||||
|
document.querySelectorAll('.role-config-panel.open').forEach(p => {
|
||||||
|
closeRolePanel(p.id.replace('rcp-', ''));
|
||||||
|
});
|
||||||
|
openRolePanel(role);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('.rcp-cancel').forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => closeRolePanel(btn.dataset.role));
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('.rcp-save').forEach(btn => {
|
||||||
|
btn.addEventListener('click', async () => {
|
||||||
|
const role = btn.dataset.role;
|
||||||
|
const panel = document.getElementById(`rcp-${role}`);
|
||||||
|
const ta = panel.querySelector('.rcp-textarea');
|
||||||
|
const checks = [...panel.querySelectorAll('.rcp-check input[type=checkbox]')];
|
||||||
|
const allChecked = checks.every(c => c.checked);
|
||||||
|
const someChecked = checks.some(c => c.checked);
|
||||||
|
const tools = allChecked ? null : (someChecked ? checks.filter(c => c.checked).map(c => c.value) : []);
|
||||||
|
|
||||||
|
btn.disabled = true;
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/models/role-config', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify({role, system_append: ta.value, tools}),
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.ok) {
|
||||||
|
// Update local state so re-open shows current values
|
||||||
|
if (!ROLE_CONFIG_DATA[role]) ROLE_CONFIG_DATA[role] = {};
|
||||||
|
ROLE_CONFIG_DATA[role].system_append = ta.value;
|
||||||
|
ROLE_CONFIG_DATA[role].tools = tools;
|
||||||
|
showToast(`${role} config saved`);
|
||||||
|
closeRolePanel(role);
|
||||||
|
} else {
|
||||||
|
showToast(data.error || 'Save failed', true);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
showToast(e.message, true);
|
||||||
|
} finally {
|
||||||
|
btn.disabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// ── Provider tabs ─────────────────────────────────────────────────────────
|
// ── Provider tabs ─────────────────────────────────────────────────────────
|
||||||
const providerVal = document.getElementById('add-provider-val');
|
const providerVal = document.getElementById('add-provider-val');
|
||||||
const pfields = {
|
const pfields = {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -11,6 +11,7 @@ API: V3 CRUD — POST /v3/crud/journal_entry/search, POST /v3/crud/journal/{id}
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
from google.genai import types
|
||||||
from config import settings
|
from config import settings
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -580,3 +581,167 @@ def _sync_journal_entry_prepend(entry_id: str, content: str, heading: str) -> st
|
|||||||
if result != "ok":
|
if result != "ok":
|
||||||
return result
|
return result
|
||||||
return f"Prepended to journal entry `{entry_id}` under heading \"{section_heading}\"."
|
return f"Prepended to journal entry `{entry_id}` under heading \"{section_heading}\"."
|
||||||
|
|
||||||
|
|
||||||
|
DECLARATIONS = [
|
||||||
|
types.FunctionDeclaration(
|
||||||
|
name="ae_journal_list",
|
||||||
|
description=(
|
||||||
|
"List all Aether Journals available for this account. "
|
||||||
|
"Returns each journal's name and id_random. "
|
||||||
|
"Call this first when you need to write a new entry or scope a search to a specific journal "
|
||||||
|
"and don't already know the journal's id."
|
||||||
|
),
|
||||||
|
parameters=types.Schema(type=types.Type.OBJECT, properties={}),
|
||||||
|
),
|
||||||
|
types.FunctionDeclaration(
|
||||||
|
name="ae_journal_search",
|
||||||
|
description=(
|
||||||
|
"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(
|
||||||
|
type=types.Type.OBJECT,
|
||||||
|
properties={
|
||||||
|
"query": types.Schema(type=types.Type.STRING, description="Fulltext keyword search. Supports boolean mode: +required -excluded \"exact phrase\"."),
|
||||||
|
"journal_id": types.Schema(type=types.Type.STRING, 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="Number of results per page (default 10)."),
|
||||||
|
"page": types.Schema(type=types.Type.INTEGER, description="Page number for pagination (default 1)."),
|
||||||
|
},
|
||||||
|
required=[],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
types.FunctionDeclaration(
|
||||||
|
name="ae_journal_entry_read",
|
||||||
|
description=(
|
||||||
|
"Fetch the full content of a single journal entry by its id_random. "
|
||||||
|
"Use this when you need to read an entry before editing it, or when search results "
|
||||||
|
"don't show enough content. Returns title, journal, tags, summary, and full content."
|
||||||
|
),
|
||||||
|
parameters=types.Schema(
|
||||||
|
type=types.Type.OBJECT,
|
||||||
|
properties={
|
||||||
|
"entry_id": types.Schema(type=types.Type.STRING, description="The id_random of the journal entry to read."),
|
||||||
|
"max_content_chars": types.Schema(type=types.Type.INTEGER, description="Maximum characters of content to return (default 4000). Increase for long entries."),
|
||||||
|
},
|
||||||
|
required=["entry_id"],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
types.FunctionDeclaration(
|
||||||
|
name="ae_journal_entries_list",
|
||||||
|
description=(
|
||||||
|
"List entries in a specific journal, newest first. "
|
||||||
|
"Use this to browse what's in a journal when you don't have a search keyword, "
|
||||||
|
"or to find entries by browsing rather than searching. "
|
||||||
|
"Returns numbered entries with id, title, tags, summary, and date."
|
||||||
|
),
|
||||||
|
parameters=types.Schema(
|
||||||
|
type=types.Type.OBJECT,
|
||||||
|
properties={
|
||||||
|
"journal_id": types.Schema(type=types.Type.STRING, description="The id_random of the journal to list entries from."),
|
||||||
|
"max_results": types.Schema(type=types.Type.INTEGER, description="Number of entries to return (default 20, max 50)."),
|
||||||
|
"page": types.Schema(type=types.Type.INTEGER, description="Page number for pagination (default 1)."),
|
||||||
|
},
|
||||||
|
required=["journal_id"],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
types.FunctionDeclaration(
|
||||||
|
name="ae_journal_entry_create",
|
||||||
|
description=(
|
||||||
|
"Create a new entry in an Aether Journal. "
|
||||||
|
"Use this to save notes, summaries, or any content the user wants to store. "
|
||||||
|
"Always call ae_journal_search first to check for existing entries on the same topic."
|
||||||
|
),
|
||||||
|
parameters=types.Schema(
|
||||||
|
type=types.Type.OBJECT,
|
||||||
|
properties={
|
||||||
|
"journal_id": types.Schema(type=types.Type.STRING, description="The id_random of the target journal. Ask the user which journal to write to if not specified."),
|
||||||
|
"title": types.Schema(type=types.Type.STRING, description="Entry title"),
|
||||||
|
"content": types.Schema(type=types.Type.STRING, description="Full entry content (markdown supported)"),
|
||||||
|
"summary": types.Schema(type=types.Type.STRING, description="Optional short summary (1-2 sentences)"),
|
||||||
|
"tags": types.Schema(type=types.Type.STRING, description="Optional comma-separated tags (e.g. 'wireguard, networking, homelab')"),
|
||||||
|
},
|
||||||
|
required=["journal_id", "title", "content"],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
types.FunctionDeclaration(
|
||||||
|
name="ae_journal_entry_update",
|
||||||
|
description=(
|
||||||
|
"Update fields on an existing journal entry. Only the fields you provide are changed — "
|
||||||
|
"omitted fields are left as-is. Use ae_journal_search to find the entry_id first. "
|
||||||
|
"To soft-delete, use ae_journal_entry_disable instead."
|
||||||
|
),
|
||||||
|
parameters=types.Schema(
|
||||||
|
type=types.Type.OBJECT,
|
||||||
|
properties={
|
||||||
|
"entry_id": types.Schema(type=types.Type.STRING, description="Journal entry id_random"),
|
||||||
|
"title": types.Schema(type=types.Type.STRING, description="New title"),
|
||||||
|
"content": types.Schema(type=types.Type.STRING, description="Replacement content (full, markdown supported)"),
|
||||||
|
"summary": types.Schema(type=types.Type.STRING, description="New summary"),
|
||||||
|
"tags": types.Schema(type=types.Type.STRING, description="Replacement comma-separated tags"),
|
||||||
|
"enable": types.Schema(type=types.Type.BOOLEAN, description="Set false to hide/disable the entry"),
|
||||||
|
},
|
||||||
|
required=["entry_id"],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
types.FunctionDeclaration(
|
||||||
|
name="ae_journal_entry_disable",
|
||||||
|
description=(
|
||||||
|
"Soft-delete a journal entry by setting enable=false. "
|
||||||
|
"The entry is hidden but not permanently removed. "
|
||||||
|
"Use ae_journal_search to find the entry_id first."
|
||||||
|
),
|
||||||
|
parameters=types.Schema(
|
||||||
|
type=types.Type.OBJECT,
|
||||||
|
properties={
|
||||||
|
"entry_id": types.Schema(type=types.Type.STRING, description="Journal entry id_random"),
|
||||||
|
},
|
||||||
|
required=["entry_id"],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
types.FunctionDeclaration(
|
||||||
|
name="ae_journal_entry_append",
|
||||||
|
description=(
|
||||||
|
"Append a new section to the bottom of a journal entry's content. "
|
||||||
|
"Each section gets a UTC timestamp heading unless you provide one. "
|
||||||
|
"Ideal for timestamped logs, running notes, or data logs."
|
||||||
|
),
|
||||||
|
parameters=types.Schema(
|
||||||
|
type=types.Type.OBJECT,
|
||||||
|
properties={
|
||||||
|
"entry_id": types.Schema(type=types.Type.STRING, description="Journal entry id_random"),
|
||||||
|
"content": types.Schema(type=types.Type.STRING, description="The text to append (markdown supported)"),
|
||||||
|
"heading": types.Schema(type=types.Type.STRING, description="Optional section heading (defaults to current UTC timestamp)"),
|
||||||
|
},
|
||||||
|
required=["entry_id", "content"],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
types.FunctionDeclaration(
|
||||||
|
name="ae_journal_entry_prepend",
|
||||||
|
description=(
|
||||||
|
"Prepend a new section to the top of a journal entry's content. "
|
||||||
|
"Each section gets a UTC timestamp heading unless you provide one. "
|
||||||
|
"Useful for most-recent-first logs."
|
||||||
|
),
|
||||||
|
parameters=types.Schema(
|
||||||
|
type=types.Type.OBJECT,
|
||||||
|
properties={
|
||||||
|
"entry_id": types.Schema(type=types.Type.STRING, description="Journal entry id_random"),
|
||||||
|
"content": types.Schema(type=types.Type.STRING, description="The text to prepend (markdown supported)"),
|
||||||
|
"heading": types.Schema(type=types.Type.STRING, description="Optional section heading (defaults to current UTC timestamp)"),
|
||||||
|
},
|
||||||
|
required=["entry_id", "content"],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from google.genai import types
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Resolved at import time — agents_sync is always at ~/agents_sync on this machine.
|
# Resolved at import time — agents_sync is always at ~/agents_sync on this machine.
|
||||||
@@ -98,3 +100,20 @@ def _read_bucket(bucket_dir: Path) -> list[dict]:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning("Failed to read task file %s: %s", path, e)
|
logger.warning("Failed to read task file %s: %s", path, e)
|
||||||
return tasks
|
return tasks
|
||||||
|
|
||||||
|
|
||||||
|
DECLARATIONS = [
|
||||||
|
types.FunctionDeclaration(
|
||||||
|
name="ae_task_list",
|
||||||
|
description=(
|
||||||
|
"List tasks from the agents_sync Kanban board (todo and in-progress). "
|
||||||
|
"Use this when asked about current work, pending tasks, or project status."
|
||||||
|
),
|
||||||
|
parameters=types.Schema(
|
||||||
|
type=types.Type.OBJECT,
|
||||||
|
properties={
|
||||||
|
"include_done": types.Schema(type=types.Type.BOOLEAN, description="If true, also include completed tasks (default false)"),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import secrets
|
|||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from google.genai import types
|
||||||
from persona import persona_path, get_user, get_persona
|
from persona import persona_path, get_user, get_persona
|
||||||
from cron_runner import load_crons, save_crons, parse_schedule
|
from cron_runner import load_crons, save_crons, parse_schedule
|
||||||
|
|
||||||
@@ -194,3 +195,64 @@ async def cron_toggle(cron_id: str) -> str:
|
|||||||
|
|
||||||
async def reminders_clear() -> str:
|
async def reminders_clear() -> str:
|
||||||
return await asyncio.to_thread(_reminders_clear)
|
return await asyncio.to_thread(_reminders_clear)
|
||||||
|
|
||||||
|
|
||||||
|
DECLARATIONS = [
|
||||||
|
types.FunctionDeclaration(
|
||||||
|
name="cron_list",
|
||||||
|
description=(
|
||||||
|
"List all scheduled cron jobs — their ID, label, schedule, type, and last run time. "
|
||||||
|
"Use this to see what's scheduled before adding or removing jobs."
|
||||||
|
),
|
||||||
|
parameters=types.Schema(type=types.Type.OBJECT, properties={}),
|
||||||
|
),
|
||||||
|
types.FunctionDeclaration(
|
||||||
|
name="cron_add",
|
||||||
|
description=(
|
||||||
|
"Create a new scheduled cron job and register it immediately (no restart needed). "
|
||||||
|
"Two types: 'remind' writes to the pending reminders queue (Inara sees it automatically "
|
||||||
|
"in context next session); 'note' appends to the scratchpad. "
|
||||||
|
"Schedule formats: 'hourly' | 'daily' | 'daily:HH:MM' | 'weekly:DOW' | 'weekly:DOW:HH:MM'. "
|
||||||
|
"Example: schedule='daily:09:00', type='remind', payload='Check in with Scott.'"
|
||||||
|
),
|
||||||
|
parameters=types.Schema(
|
||||||
|
type=types.Type.OBJECT,
|
||||||
|
properties={
|
||||||
|
"label": types.Schema(type=types.Type.STRING, description="Short human-readable name for this job (e.g. 'Morning check-in')"),
|
||||||
|
"schedule": types.Schema(type=types.Type.STRING, description="When to run. Formats: hourly | daily | daily:HH:MM | weekly:DOW | weekly:DOW:HH:MM (e.g. 'weekly:mon:09:00')"),
|
||||||
|
"job_type": types.Schema(type=types.Type.STRING, description="'remind' (→ REMINDERS.md, auto-surfaced in context) or 'note' (→ SCRATCH.md)"),
|
||||||
|
"payload": types.Schema(type=types.Type.STRING, description="The text to write when the job fires"),
|
||||||
|
},
|
||||||
|
required=["label", "schedule", "job_type", "payload"],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
types.FunctionDeclaration(
|
||||||
|
name="cron_remove",
|
||||||
|
description=(
|
||||||
|
"Permanently delete a scheduled cron job. Use cron_list first to get the ID. "
|
||||||
|
"To temporarily disable without deleting, use cron_toggle instead."
|
||||||
|
),
|
||||||
|
parameters=types.Schema(
|
||||||
|
type=types.Type.OBJECT,
|
||||||
|
properties={
|
||||||
|
"cron_id": types.Schema(type=types.Type.STRING, description="Job ID (e.g. c_abc123) — get from cron_list"),
|
||||||
|
},
|
||||||
|
required=["cron_id"],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
types.FunctionDeclaration(
|
||||||
|
name="cron_toggle",
|
||||||
|
description=(
|
||||||
|
"Pause a running cron job, or resume a paused one. "
|
||||||
|
"The job stays in the list and can be re-enabled later. "
|
||||||
|
"Use cron_list to see current enabled/paused state."
|
||||||
|
),
|
||||||
|
parameters=types.Schema(
|
||||||
|
type=types.Type.OBJECT,
|
||||||
|
properties={
|
||||||
|
"cron_id": types.Schema(type=types.Type.STRING, description="Job ID (e.g. c_abc123) — get from cron_list"),
|
||||||
|
},
|
||||||
|
required=["cron_id"],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import asyncio
|
|||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from google.genai import types
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Directories the orchestrator is allowed to read from.
|
# Directories the orchestrator is allowed to read from.
|
||||||
@@ -212,3 +214,57 @@ def _sync_file_write(path: str, content: str, mode: str) -> str:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("file_write error for %s: %s", resolved, e)
|
logger.error("file_write error for %s: %s", resolved, e)
|
||||||
return f"Write error: {e}"
|
return f"Write error: {e}"
|
||||||
|
|
||||||
|
|
||||||
|
DECLARATIONS = [
|
||||||
|
types.FunctionDeclaration(
|
||||||
|
name="file_read",
|
||||||
|
description=(
|
||||||
|
"Read a local file and return its contents. "
|
||||||
|
"Allowed directories: ~/agents_sync/, ~/OSIT_dev/, ~/DgrZone_Nextcloud/, ~/OSIT_Nextcloud/. "
|
||||||
|
"Use this to read documentation, notes, CLAUDE.md files, or config references. "
|
||||||
|
"If given a directory path, returns a directory listing instead."
|
||||||
|
),
|
||||||
|
parameters=types.Schema(
|
||||||
|
type=types.Type.OBJECT,
|
||||||
|
properties={
|
||||||
|
"path": types.Schema(type=types.Type.STRING, description="Absolute or home-relative path to the file (e.g. ~/agents_sync/CLAUDE.md or /home/scott/agents_sync/tasks/01_todo/)"),
|
||||||
|
"max_lines": types.Schema(type=types.Type.INTEGER, description="Optional line limit (default 500)"),
|
||||||
|
},
|
||||||
|
required=["path"],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
types.FunctionDeclaration(
|
||||||
|
name="file_list",
|
||||||
|
description=(
|
||||||
|
"List the files and subdirectories in a directory. "
|
||||||
|
"Allowed paths: ~/agents_sync/, ~/OSIT_dev/, ~/DgrZone_Nextcloud/, ~/OSIT_Nextcloud/. "
|
||||||
|
"ADMIN ONLY."
|
||||||
|
),
|
||||||
|
parameters=types.Schema(
|
||||||
|
type=types.Type.OBJECT,
|
||||||
|
properties={
|
||||||
|
"path": types.Schema(type=types.Type.STRING, description="Absolute or home-relative path to the directory"),
|
||||||
|
},
|
||||||
|
required=["path"],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
types.FunctionDeclaration(
|
||||||
|
name="file_write",
|
||||||
|
description=(
|
||||||
|
"Write or append content to a file. "
|
||||||
|
"Write-allowed paths: ~/agents_sync/ and the Cortex home/ directory. "
|
||||||
|
"Creates parent directories if needed. "
|
||||||
|
"ADMIN ONLY. Requires user confirmation before executing."
|
||||||
|
),
|
||||||
|
parameters=types.Schema(
|
||||||
|
type=types.Type.OBJECT,
|
||||||
|
properties={
|
||||||
|
"path": types.Schema(type=types.Type.STRING, description="Absolute or home-relative path to write to"),
|
||||||
|
"content": types.Schema(type=types.Type.STRING, description="Content to write"),
|
||||||
|
"mode": types.Schema(type=types.Type.STRING, description="'overwrite' (default, replaces file) or 'append' (adds to end)"),
|
||||||
|
},
|
||||||
|
required=["path", "content"],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
from google.genai import types
|
||||||
from config import settings
|
from config import settings
|
||||||
from persona import get_user
|
from persona import get_user
|
||||||
|
|
||||||
@@ -80,3 +81,40 @@ async def nc_talk_send(message: str) -> str:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning("nc_talk_send error for %s: %s", username, e)
|
logger.warning("nc_talk_send error for %s: %s", username, e)
|
||||||
return f"Failed to send notification: {e}"
|
return f"Failed to send notification: {e}"
|
||||||
|
|
||||||
|
|
||||||
|
DECLARATIONS = [
|
||||||
|
types.FunctionDeclaration(
|
||||||
|
name="email_send",
|
||||||
|
description=(
|
||||||
|
"Send an email from the server's configured SMTP account. Use for delivering "
|
||||||
|
"summaries, reports, reminders, or any content the user wants emailed. "
|
||||||
|
"body is plain text; newlines are preserved."
|
||||||
|
),
|
||||||
|
parameters=types.Schema(
|
||||||
|
type=types.Type.OBJECT,
|
||||||
|
properties={
|
||||||
|
"to": types.Schema(type=types.Type.STRING, description="Recipient email address"),
|
||||||
|
"subject": types.Schema(type=types.Type.STRING, description="Email subject line"),
|
||||||
|
"body": types.Schema(type=types.Type.STRING, description="Plain-text email body"),
|
||||||
|
},
|
||||||
|
required=["to", "subject", "body"],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
types.FunctionDeclaration(
|
||||||
|
name="nc_talk_send",
|
||||||
|
description=(
|
||||||
|
"Send a proactive message to the user via their configured notification channel "
|
||||||
|
"(Nextcloud Talk by default). Use this to notify the user of completed background "
|
||||||
|
"tasks, important events, or anything they should know between sessions. "
|
||||||
|
"Requires notification_channel and notification_room set in channels.json."
|
||||||
|
),
|
||||||
|
parameters=types.Schema(
|
||||||
|
type=types.Type.OBJECT,
|
||||||
|
properties={
|
||||||
|
"message": types.Schema(type=types.Type.STRING, description="The message to send to the user"),
|
||||||
|
},
|
||||||
|
required=["message"],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import asyncio
|
|||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from google.genai import types
|
||||||
from persona import persona_path
|
from persona import persona_path
|
||||||
|
|
||||||
|
|
||||||
@@ -124,3 +125,55 @@ async def reminders_remove(index: int) -> str:
|
|||||||
|
|
||||||
async def reminders_clear() -> str:
|
async def reminders_clear() -> str:
|
||||||
return await asyncio.to_thread(_reminders_clear)
|
return await asyncio.to_thread(_reminders_clear)
|
||||||
|
|
||||||
|
|
||||||
|
DECLARATIONS = [
|
||||||
|
types.FunctionDeclaration(
|
||||||
|
name="reminders_add",
|
||||||
|
description=(
|
||||||
|
"Add a new reminder to REMINDERS.md. Reminders are automatically surfaced "
|
||||||
|
"in your context at the start of each session (Tier 2+). "
|
||||||
|
"Use this when the user asks you to remember something, follow up on something, "
|
||||||
|
"or surface a note at the next session."
|
||||||
|
),
|
||||||
|
parameters=types.Schema(
|
||||||
|
type=types.Type.OBJECT,
|
||||||
|
properties={
|
||||||
|
"text": types.Schema(type=types.Type.STRING, description="The reminder text to add"),
|
||||||
|
"label": types.Schema(type=types.Type.STRING, description="Optional heading for this reminder (e.g. 'Follow up on NC Talk'). Defaults to current timestamp."),
|
||||||
|
},
|
||||||
|
required=["text"],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
types.FunctionDeclaration(
|
||||||
|
name="reminders_list",
|
||||||
|
description=(
|
||||||
|
"Read all current pending reminders from REMINDERS.md. "
|
||||||
|
"Use this to check what reminders are queued before adding duplicates, "
|
||||||
|
"or to show the user what's pending."
|
||||||
|
),
|
||||||
|
parameters=types.Schema(type=types.Type.OBJECT, properties={}),
|
||||||
|
),
|
||||||
|
types.FunctionDeclaration(
|
||||||
|
name="reminders_remove",
|
||||||
|
description=(
|
||||||
|
"Remove a single reminder by its number. "
|
||||||
|
"Call reminders_list first to get the numbered list, then pass the number of the reminder to remove."
|
||||||
|
),
|
||||||
|
parameters=types.Schema(
|
||||||
|
type=types.Type.OBJECT,
|
||||||
|
properties={
|
||||||
|
"index": types.Schema(type=types.Type.INTEGER, description="The number of the reminder to remove (1 = first item in reminders_list output)."),
|
||||||
|
},
|
||||||
|
required=["index"],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
types.FunctionDeclaration(
|
||||||
|
name="reminders_clear",
|
||||||
|
description=(
|
||||||
|
"Erase all pending reminders from REMINDERS.md. "
|
||||||
|
"Use this after you have acknowledged and acted on the reminders shown in your context."
|
||||||
|
),
|
||||||
|
parameters=types.Schema(type=types.Type.OBJECT, properties={}),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import asyncio
|
|||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from google.genai import types
|
||||||
from persona import persona_path
|
from persona import persona_path
|
||||||
|
|
||||||
|
|
||||||
@@ -77,3 +78,51 @@ async def scratch_append(content: str, heading: str | None = None) -> str:
|
|||||||
|
|
||||||
async def scratch_clear() -> str:
|
async def scratch_clear() -> str:
|
||||||
return await asyncio.to_thread(_scratch_clear)
|
return await asyncio.to_thread(_scratch_clear)
|
||||||
|
|
||||||
|
|
||||||
|
DECLARATIONS = [
|
||||||
|
types.FunctionDeclaration(
|
||||||
|
name="scratch_read",
|
||||||
|
description=(
|
||||||
|
"Read the full contents of the scratchpad. "
|
||||||
|
"Use this to recall working notes, mid-task context, or anything previously jotted down. "
|
||||||
|
"The scratchpad is transient — nothing here is distilled or archived."
|
||||||
|
),
|
||||||
|
parameters=types.Schema(type=types.Type.OBJECT, properties={}),
|
||||||
|
),
|
||||||
|
types.FunctionDeclaration(
|
||||||
|
name="scratch_write",
|
||||||
|
description=(
|
||||||
|
"Replace the entire scratchpad with new content. "
|
||||||
|
"Use this to set a clean working note, replacing whatever was there before. "
|
||||||
|
"For adding without replacing, use scratch_append instead."
|
||||||
|
),
|
||||||
|
parameters=types.Schema(
|
||||||
|
type=types.Type.OBJECT,
|
||||||
|
properties={
|
||||||
|
"content": types.Schema(type=types.Type.STRING, description="The new scratchpad content (markdown supported)"),
|
||||||
|
},
|
||||||
|
required=["content"],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
types.FunctionDeclaration(
|
||||||
|
name="scratch_append",
|
||||||
|
description=(
|
||||||
|
"Add a new section to the bottom of the scratchpad without replacing existing content. "
|
||||||
|
"Each section gets a timestamp heading unless you supply one."
|
||||||
|
),
|
||||||
|
parameters=types.Schema(
|
||||||
|
type=types.Type.OBJECT,
|
||||||
|
properties={
|
||||||
|
"content": types.Schema(type=types.Type.STRING, description="The content to append (markdown supported)"),
|
||||||
|
"heading": types.Schema(type=types.Type.STRING, description="Optional section heading. Defaults to current UTC timestamp."),
|
||||||
|
},
|
||||||
|
required=["content"],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
types.FunctionDeclaration(
|
||||||
|
name="scratch_clear",
|
||||||
|
description="Erase everything in the scratchpad. Use when the working notes are no longer needed.",
|
||||||
|
parameters=types.Schema(type=types.Type.OBJECT, properties={}),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import os
|
|||||||
import subprocess
|
import subprocess
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from google.genai import types
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Absolute paths — resolved relative to this file so they work regardless of cwd
|
# Absolute paths — resolved relative to this file so they work regardless of cwd
|
||||||
@@ -246,3 +248,87 @@ async def cortex_update() -> str:
|
|||||||
lines.append("Call `cortex_restart` to apply the update.")
|
lines.append("Call `cortex_restart` to apply the update.")
|
||||||
|
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
DECLARATIONS = [
|
||||||
|
types.FunctionDeclaration(
|
||||||
|
name="shell_exec",
|
||||||
|
description=(
|
||||||
|
"Execute a shell command on the Cortex host machine and return its output. "
|
||||||
|
"Use for system diagnostics: disk usage (df -h), process status (ps aux), "
|
||||||
|
"directory listings (ls), memory (free -h), uptime, network info, log tails, etc. "
|
||||||
|
"Commands run as the Cortex service user. Timeout enforced (default 30s, max 120s). "
|
||||||
|
"Avoid destructive commands — prefer read-only system queries."
|
||||||
|
),
|
||||||
|
parameters=types.Schema(
|
||||||
|
type=types.Type.OBJECT,
|
||||||
|
properties={
|
||||||
|
"command": types.Schema(type=types.Type.STRING, description="Shell command to run (e.g. 'df -h', 'ls ~/agents_sync/', 'journalctl --user -u cortex -n 50')"),
|
||||||
|
"working_dir": types.Schema(type=types.Type.STRING, description="Optional working directory (e.g. '~/agents_sync/projects'). Defaults to home directory."),
|
||||||
|
"timeout": types.Schema(type=types.Type.INTEGER, description="Timeout in seconds (default 30, max 120)"),
|
||||||
|
},
|
||||||
|
required=["command"],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
types.FunctionDeclaration(
|
||||||
|
name="claude_allow_dir",
|
||||||
|
description=(
|
||||||
|
"Add a directory to Claude Code's auto-allow list so Claude can read or write "
|
||||||
|
"files there without prompting. Edits ~/.claude/settings.json on the local machine. "
|
||||||
|
"Use this when Claude is silently hanging or being blocked from accessing a directory. "
|
||||||
|
"Changes take effect in the next Claude Code session."
|
||||||
|
),
|
||||||
|
parameters=types.Schema(
|
||||||
|
type=types.Type.OBJECT,
|
||||||
|
properties={
|
||||||
|
"path": types.Schema(type=types.Type.STRING, description="Absolute or home-relative path to the directory (e.g. ~/OSIT_dev/aether_api_fastapi or /home/scott/agents_sync)"),
|
||||||
|
"mode": types.Schema(type=types.Type.STRING, description="Permission mode: 'r' (read-only), 'w' (write-only), or 'rw' (both). Default: rw"),
|
||||||
|
},
|
||||||
|
required=["path"],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
types.FunctionDeclaration(
|
||||||
|
name="cortex_restart",
|
||||||
|
description=(
|
||||||
|
"Restart the Cortex service via systemd. Schedules a restart 5 seconds from now. "
|
||||||
|
"The current connection will drop — inform the user to refresh the page. "
|
||||||
|
"Use after config changes, memory edits, or when the service needs a fresh start. "
|
||||||
|
"ADMIN ONLY."
|
||||||
|
),
|
||||||
|
parameters=types.Schema(type=types.Type.OBJECT, properties={}),
|
||||||
|
),
|
||||||
|
types.FunctionDeclaration(
|
||||||
|
name="cortex_logs",
|
||||||
|
description=(
|
||||||
|
"Fetch recent lines from the Cortex systemd service journal. "
|
||||||
|
"Use for debugging errors, checking startup status, or reviewing recent activity. "
|
||||||
|
"ADMIN ONLY."
|
||||||
|
),
|
||||||
|
parameters=types.Schema(
|
||||||
|
type=types.Type.OBJECT,
|
||||||
|
properties={
|
||||||
|
"lines": types.Schema(type=types.Type.INTEGER, description="Number of log lines to return (default 50, max 200)"),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
types.FunctionDeclaration(
|
||||||
|
name="cortex_status",
|
||||||
|
description=(
|
||||||
|
"Return Cortex service status: current git branch and commit, how many commits "
|
||||||
|
"ahead/behind the remote, and the systemctl service state. "
|
||||||
|
"Use to check what version is running or whether the service is healthy. "
|
||||||
|
"ADMIN ONLY."
|
||||||
|
),
|
||||||
|
parameters=types.Schema(type=types.Type.OBJECT, properties={}),
|
||||||
|
),
|
||||||
|
types.FunctionDeclaration(
|
||||||
|
name="cortex_update",
|
||||||
|
description=(
|
||||||
|
"Pull the latest code from git, run a syntax check on all Python files, and report "
|
||||||
|
"what changed. Does NOT restart automatically — call cortex_restart separately after "
|
||||||
|
"reviewing the output. Will report syntax errors if the pull introduces broken code. "
|
||||||
|
"ADMIN ONLY. Requires confirmation."
|
||||||
|
),
|
||||||
|
parameters=types.Schema(type=types.Type.OBJECT, properties={}),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import asyncio
|
|||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from google.genai import types
|
||||||
from persona import persona_path
|
from persona import persona_path
|
||||||
|
|
||||||
|
|
||||||
@@ -133,3 +134,70 @@ async def task_update(task_id: str, status: str | None = None, title: str | None
|
|||||||
|
|
||||||
async def task_complete(task_id: str) -> str:
|
async def task_complete(task_id: str) -> str:
|
||||||
return await asyncio.to_thread(_task_complete, task_id)
|
return await asyncio.to_thread(_task_complete, task_id)
|
||||||
|
|
||||||
|
|
||||||
|
DECLARATIONS = [
|
||||||
|
types.FunctionDeclaration(
|
||||||
|
name="task_list",
|
||||||
|
description=(
|
||||||
|
"List personal tasks from Inara's task list. "
|
||||||
|
"Use this to check what's on the list, review pending work, or find a task ID. "
|
||||||
|
"Optionally filter by status: 'todo', 'in_progress', or 'done'."
|
||||||
|
),
|
||||||
|
parameters=types.Schema(
|
||||||
|
type=types.Type.OBJECT,
|
||||||
|
properties={
|
||||||
|
"status": types.Schema(type=types.Type.STRING, description="Filter by status: 'todo', 'in_progress', or 'done'. Omit to list all."),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
types.FunctionDeclaration(
|
||||||
|
name="task_create",
|
||||||
|
description=(
|
||||||
|
"Add a new task to Inara's personal task list. "
|
||||||
|
"Use this when the user asks to remember something, add a to-do, or track a follow-up."
|
||||||
|
),
|
||||||
|
parameters=types.Schema(
|
||||||
|
type=types.Type.OBJECT,
|
||||||
|
properties={
|
||||||
|
"title": types.Schema(type=types.Type.STRING, description="Short task title"),
|
||||||
|
"description": types.Schema(type=types.Type.STRING, description="Optional longer description or context"),
|
||||||
|
"priority": types.Schema(type=types.Type.STRING, description="Priority: 'low', 'normal', or 'high'. Default: normal."),
|
||||||
|
},
|
||||||
|
required=["title"],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
types.FunctionDeclaration(
|
||||||
|
name="task_update",
|
||||||
|
description=(
|
||||||
|
"Update an existing task. Use task_list first to get the task ID. "
|
||||||
|
"Can update status, title, description, or priority. "
|
||||||
|
"To just mark complete, use task_complete instead."
|
||||||
|
),
|
||||||
|
parameters=types.Schema(
|
||||||
|
type=types.Type.OBJECT,
|
||||||
|
properties={
|
||||||
|
"task_id": types.Schema(type=types.Type.STRING, description="Task ID (e.g. t_abc123) — get from task_list"),
|
||||||
|
"status": types.Schema(type=types.Type.STRING, description="New status: 'todo', 'in_progress', or 'done'"),
|
||||||
|
"title": types.Schema(type=types.Type.STRING, description="Updated title"),
|
||||||
|
"description": types.Schema(type=types.Type.STRING, description="Updated description"),
|
||||||
|
"priority": types.Schema(type=types.Type.STRING, description="Updated priority: 'low', 'normal', or 'high'"),
|
||||||
|
},
|
||||||
|
required=["task_id"],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
types.FunctionDeclaration(
|
||||||
|
name="task_complete",
|
||||||
|
description=(
|
||||||
|
"Mark a task as done. Use task_list first to get the task ID. "
|
||||||
|
"Shorthand for task_update with status='done'."
|
||||||
|
),
|
||||||
|
parameters=types.Schema(
|
||||||
|
type=types.Type.OBJECT,
|
||||||
|
properties={
|
||||||
|
"task_id": types.Schema(type=types.Type.STRING, description="Task ID (e.g. t_abc123) — get from task_list"),
|
||||||
|
},
|
||||||
|
required=["task_id"],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import asyncio
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
|
from google.genai import types
|
||||||
|
|
||||||
from config import settings
|
from config import settings
|
||||||
|
|
||||||
@@ -74,3 +75,41 @@ async def http_fetch(
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning("http_fetch error for %s: %s", url, e)
|
logger.warning("http_fetch error for %s: %s", url, e)
|
||||||
return f"Error: {e}"
|
return f"Error: {e}"
|
||||||
|
|
||||||
|
|
||||||
|
DECLARATIONS = [
|
||||||
|
types.FunctionDeclaration(
|
||||||
|
name="web_search",
|
||||||
|
description=(
|
||||||
|
"Search the web for current information. Use this when you need up-to-date "
|
||||||
|
"facts, news, documentation, or anything not in your training data."
|
||||||
|
),
|
||||||
|
parameters=types.Schema(
|
||||||
|
type=types.Type.OBJECT,
|
||||||
|
properties={
|
||||||
|
"query": types.Schema(type=types.Type.STRING, description="The search query string"),
|
||||||
|
"max_results": types.Schema(type=types.Type.INTEGER, description="Number of results to return (default 5, max 10)"),
|
||||||
|
},
|
||||||
|
required=["query"],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
types.FunctionDeclaration(
|
||||||
|
name="http_fetch",
|
||||||
|
description=(
|
||||||
|
"Fetch a specific URL and return the response. Unlike web_search, this hits "
|
||||||
|
"a direct URL — useful for health checks, JSON API endpoints, webhook testing, "
|
||||||
|
"or reading a specific page when you already know the URL. "
|
||||||
|
"Response body is capped at 8 KB."
|
||||||
|
),
|
||||||
|
parameters=types.Schema(
|
||||||
|
type=types.Type.OBJECT,
|
||||||
|
properties={
|
||||||
|
"url": types.Schema(type=types.Type.STRING, description="Full URL to fetch"),
|
||||||
|
"method": types.Schema(type=types.Type.STRING, description="HTTP method: GET (default), POST, HEAD"),
|
||||||
|
"body": types.Schema(type=types.Type.STRING, description="Optional request body (for POST requests)"),
|
||||||
|
"timeout": types.Schema(type=types.Type.INTEGER, description="Request timeout in seconds (default 15, max 60)"),
|
||||||
|
},
|
||||||
|
required=["url"],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user