mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-07-09 12:07:18 +00:00
Checkpoint Odysseus local update
This commit is contained in:
+23
-10
@@ -108,16 +108,28 @@ async def do_manage_contact(content: str, owner: Optional[str] = None) -> Dict:
|
||||
|
||||
if action == "add":
|
||||
email = (args.get("email") or "").strip()
|
||||
if not email:
|
||||
return {"error": "email is required for add", "exit_code": 1}
|
||||
name = (args.get("name") or "").strip() or email.split("@")[0]
|
||||
# Dedupe by email (same as the /add route).
|
||||
phones = [str(p or "").strip() for p in (args.get("phones") or []) if str(p or "").strip()]
|
||||
phone = (args.get("phone") or "").strip()
|
||||
if phone and phone not in phones:
|
||||
phones.insert(0, phone)
|
||||
address = (args.get("address") or "").strip()
|
||||
name = (args.get("name") or "").strip()
|
||||
if not name and email:
|
||||
name = email.split("@")[0]
|
||||
if not name and not email and not phones and not address:
|
||||
return {"error": "name plus email, phone, or address is required for add", "exit_code": 1}
|
||||
if not name:
|
||||
name = email.split("@")[0] if email else (phones[0] if phones else "Contact")
|
||||
# Dedupe by email or phone (same as the /add route).
|
||||
existing = await asyncio.to_thread(cc._fetch_contacts)
|
||||
for c in existing:
|
||||
if email.lower() in [e.lower() for e in c.get("emails", [])]:
|
||||
if email and email.lower() in [e.lower() for e in c.get("emails", [])]:
|
||||
return {"output": f"{email} is already a contact ({c.get('name','')}).", "exit_code": 0}
|
||||
ok = await asyncio.to_thread(cc._create_contact, name, email)
|
||||
return {"output": f"{'Added' if ok else 'Failed to add'} {name} <{email}>.", "exit_code": 0 if ok else 1}
|
||||
if phones and any(p in (c.get("phones") or []) for p in phones):
|
||||
return {"output": f"{phones[0]} is already a contact ({c.get('name','')}).", "exit_code": 0}
|
||||
ok = await asyncio.to_thread(cc._create_contact, name, email, address, phones)
|
||||
detail = email or ", ".join(phones) or address
|
||||
return {"output": f"{'Added' if ok else 'Failed to add'} {name} ({detail}).", "exit_code": 0 if ok else 1}
|
||||
|
||||
if action in ("update", "edit"):
|
||||
uid = (args.get("uid") or "").strip()
|
||||
@@ -129,11 +141,12 @@ async def do_manage_contact(content: str, owner: Optional[str] = None) -> Dict:
|
||||
emails = [args["email"]]
|
||||
emails = [e.strip() for e in (emails or []) if e and e.strip()]
|
||||
phones = [p.strip() for p in (args.get("phones") or []) if p and p.strip()]
|
||||
if not name and not emails:
|
||||
return {"error": "Provide a name or emails to update", "exit_code": 1}
|
||||
address = (args.get("address") or "").strip()
|
||||
if not name and not emails and not phones and not address:
|
||||
return {"error": "Provide a name, emails, phones, or address to update", "exit_code": 1}
|
||||
if not name and emails:
|
||||
name = emails[0].split("@")[0]
|
||||
ok = await asyncio.to_thread(cc._update_contact, uid, name, emails, phones)
|
||||
ok = await asyncio.to_thread(cc._update_contact, uid, name, emails, phones, address)
|
||||
return {"output": "Contact updated." if ok else "Update failed.", "exit_code": 0 if ok else 1}
|
||||
|
||||
if action == "delete":
|
||||
|
||||
@@ -315,6 +315,7 @@ async def _cookbook_register_task(
|
||||
_MODEL_PROCESS_PATTERNS = [
|
||||
("vLLM", ["vllm.entrypoints", "vllm serve", "/vllm/", "vllm-openai"]),
|
||||
("SGLang", ["sglang.launch_server", "sglang/launch_server"]),
|
||||
("MLX", ["mlx_lm.server", "mlx-lm"]),
|
||||
("llama.cpp", ["llama-server", "llama_cpp_server", "llamacppserver"]),
|
||||
("Ollama", ["ollama serve", "ollama runner", "/ollama "]),
|
||||
("ComfyUI", ["comfyui/main.py", "/ComfyUI/main.py", "ComfyUI"]),
|
||||
@@ -590,7 +591,7 @@ async def do_serve_model(content: str, owner: Optional[str] = None) -> Dict:
|
||||
hint = ""
|
||||
if isinstance(err_msg, str) and "cmd" in err_msg.lower():
|
||||
hint = (" — the cmd must START with an allowlisted binary "
|
||||
"(vllm, python3, llama-server, ollama, sglang, lmdeploy, node, npx). "
|
||||
"(vllm, python3, llama-server, ollama, sglang, mlx_lm, lmdeploy, node, npx). "
|
||||
"Do NOT prefix with `cd …`, `source …`, or chain with `&&`. "
|
||||
"env_prefix (e.g. `source ~/qwen35-env/bin/activate`) is added "
|
||||
"automatically from the host's saved venv settings.")
|
||||
@@ -635,7 +636,7 @@ async def do_list_served_models(content: str, owner: Optional[str] = None) -> Di
|
||||
|
||||
if not merged:
|
||||
return {
|
||||
"output": "No model servers currently running (cookbook task tracker empty; /proc scan found no vLLM / sglang / llama.cpp / Ollama / ComfyUI / A1111 / Fooocus / InvokeAI / TGI / Aphrodite / Triton / Diffusers processes).",
|
||||
"output": "No model servers currently running (cookbook task tracker empty; /proc scan found no vLLM / sglang / MLX / llama.cpp / Ollama / ComfyUI / A1111 / Fooocus / InvokeAI / TGI / Aphrodite / Triton / Diffusers processes).",
|
||||
"exit_code": 0,
|
||||
}
|
||||
|
||||
|
||||
+76
-25
@@ -27,7 +27,8 @@ async def do_manage_notes(content: str, owner: Optional[str] = None) -> Dict:
|
||||
|
||||
# Action aliases — match what models actually emit. `create` is the most
|
||||
# common alternative to `add`. Hyphenated forms also accepted.
|
||||
action = (args.get("action") or "").replace("-", "_").strip().lower()
|
||||
raw_action = (args.get("action") or "").replace("-", "_").strip().lower()
|
||||
action = raw_action
|
||||
_NOTE_ACTION_ALIASES = {
|
||||
"create": "add",
|
||||
"new": "add",
|
||||
@@ -60,37 +61,68 @@ async def do_manage_notes(content: str, owner: Optional[str] = None) -> Dict:
|
||||
q = q.filter(Note.owner == owner)
|
||||
return q.first()
|
||||
|
||||
def _format_note_list(notes) -> str:
|
||||
lines = []
|
||||
for n in notes:
|
||||
pin = " [PINNED]" if n.pinned else ""
|
||||
typ = " [checklist]" if n.note_type == "checklist" else ""
|
||||
lbl = f" #{n.label}" if n.label else ""
|
||||
title = n.title or "(untitled)"
|
||||
lines.append(f"- [{n.id[:8]}] **{title}**{pin}{typ}{lbl}")
|
||||
if n.note_type == "checklist" and n.items:
|
||||
try:
|
||||
items = json.loads(n.items)
|
||||
for i, item in enumerate(items):
|
||||
mark = "x" if item.get("done") else " "
|
||||
lines.append(f" [{mark}] {i}: {item.get('text', '')}")
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
pass
|
||||
elif n.content:
|
||||
snippet = n.content[:80].replace("\n", " ")
|
||||
lines.append(f" {snippet}")
|
||||
return "\n".join(lines)
|
||||
|
||||
try:
|
||||
if action == "list":
|
||||
if action in ("list", "search", "find"):
|
||||
q = db.query(Note)
|
||||
if owner is not None:
|
||||
q = q.filter(Note.owner == owner)
|
||||
if args.get("label"):
|
||||
q = q.filter(Note.label == args["label"])
|
||||
label_filter = str(args.get("label") or "").strip()
|
||||
if label_filter and label_filter.lower() != "default":
|
||||
q = q.filter(Note.label == label_filter)
|
||||
show_archived = args.get("archived", False)
|
||||
q = q.filter(Note.archived == show_archived)
|
||||
notes = q.order_by(Note.pinned.desc(), Note.updated_at.desc()).all()
|
||||
if action in ("search", "find"):
|
||||
query = str(
|
||||
args.get("query")
|
||||
or args.get("text")
|
||||
or args.get("title")
|
||||
or args.get("content")
|
||||
or ""
|
||||
).strip().lower()
|
||||
if query:
|
||||
filtered = []
|
||||
for n in notes:
|
||||
haystack = " ".join(
|
||||
str(part or "")
|
||||
for part in (n.title, n.content, n.label, n.items)
|
||||
).lower()
|
||||
if query in haystack:
|
||||
filtered.append(n)
|
||||
notes = filtered
|
||||
if not notes:
|
||||
return {"response": "No notes found.", "exit_code": 0}
|
||||
lines = []
|
||||
for n in notes:
|
||||
pin = " [PINNED]" if n.pinned else ""
|
||||
typ = " [checklist]" if n.note_type == "checklist" else ""
|
||||
lbl = f" #{n.label}" if n.label else ""
|
||||
title = n.title or "(untitled)"
|
||||
lines.append(f"- [{n.id[:8]}] **{title}**{pin}{typ}{lbl}")
|
||||
if n.note_type == "checklist" and n.items:
|
||||
try:
|
||||
items = json.loads(n.items)
|
||||
for i, item in enumerate(items):
|
||||
mark = "x" if item.get("done") else " "
|
||||
lines.append(f" [{mark}] {i}: {item.get('text', '')}")
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
pass
|
||||
elif n.content:
|
||||
snippet = n.content[:80].replace("\n", " ")
|
||||
lines.append(f" {snippet}")
|
||||
return {"results": "\n".join(lines)}
|
||||
return {"results": _format_note_list(notes), "exit_code": 0}
|
||||
|
||||
elif action == "view":
|
||||
note_id = args.get("id", "")
|
||||
note = _note_by_prefix(note_id)
|
||||
if not note:
|
||||
return {"error": f"Note '{note_id}' not found", "exit_code": 1}
|
||||
if not _note_visible_to_owner(note, owner):
|
||||
return {"error": "Note not found", "exit_code": 1}
|
||||
return {"results": _format_note_list([note]), "exit_code": 0}
|
||||
|
||||
elif action == "add":
|
||||
# Accept the various field names models emit: `text` is the most
|
||||
@@ -120,6 +152,25 @@ async def do_manage_notes(content: str, owner: Optional[str] = None) -> Dict:
|
||||
# `new Date()` resolves the right absolute moment regardless of
|
||||
# where the user is.
|
||||
due_raw = args.get("due_date")
|
||||
if not due_raw:
|
||||
combined_text = " ".join(
|
||||
str(v or "")
|
||||
for v in (title, content_raw, text_raw)
|
||||
).strip()
|
||||
lower_combined = combined_text.lower()
|
||||
looks_like_reminder = (
|
||||
raw_action in {"remind", "reminder"}
|
||||
or re.search(r"\bremind(?:er)?\b", lower_combined)
|
||||
)
|
||||
if looks_like_reminder:
|
||||
temporal = re.search(
|
||||
r"\b(?:today|tonight|tomorrow|tmrw|yesterday)\b(?:\s+(?:at\s+)?\d{1,2}(?::\d{2})?\s*(?:am|pm)?)?"
|
||||
r"|\b\d{1,2}(?::\d{2})?\s*(?:am|pm)?\s+(?:today|tonight|tomorrow|tmrw|yesterday)\b"
|
||||
r"|\bin\s+\d+\s*(?:hour|hr|minute|min|day)s?\b",
|
||||
lower_combined,
|
||||
)
|
||||
if temporal:
|
||||
due_raw = temporal.group(0)
|
||||
due_iso = None
|
||||
if due_raw:
|
||||
try:
|
||||
@@ -170,7 +221,7 @@ async def do_manage_notes(content: str, owner: Optional[str] = None) -> Dict:
|
||||
# link with no target, leaving the user with a click that
|
||||
# did nothing and uncertainty about whether the note was made.
|
||||
return {
|
||||
"response": f"Note created: \"{title or '(untitled)'}\" (id: {note.id[:8]})",
|
||||
"response": f"{'Reminder' if due_iso else 'Note'} created: \"{title or '(untitled)'}\" (id: {note.id[:8]})",
|
||||
"note_id": note.id,
|
||||
"note_title": title or "",
|
||||
"open_url": f"/#open=notes¬e={note.id}",
|
||||
@@ -246,7 +297,7 @@ async def do_manage_notes(content: str, owner: Optional[str] = None) -> Dict:
|
||||
return {"response": f"Item '{items[index].get('text', '')}' marked {mark}", "exit_code": 0}
|
||||
|
||||
else:
|
||||
return {"error": f"Unknown action: {action}. Use list/add/update/delete/toggle_item", "exit_code": 1}
|
||||
return {"error": f"Unknown action: {action}. Use list/search/view/add/update/delete/toggle_item", "exit_code": 1}
|
||||
except Exception as e:
|
||||
logger.error(f"manage_notes error: {e}")
|
||||
return {"error": str(e), "exit_code": 1}
|
||||
|
||||
+2
-2
@@ -34,8 +34,8 @@ async def do_search_chats(query: str, limit: int = 20, owner: str | None = None)
|
||||
|
||||
lines = [f"Found {len(seen_sessions)} session(s) matching \"{query}\":\n"]
|
||||
for sid, result in seen_sessions.items():
|
||||
lines.append(f"- **{result.session_name}** (#{sid})")
|
||||
lines.append(f" Link: [Open chat](#{sid})")
|
||||
lines.append(f"- [**{result.session_name}**](#session-{sid})")
|
||||
lines.append(f" Open: [Open chat](#session-{sid})")
|
||||
lines.append(f" Match ({result.role}): {result.content_snippet}")
|
||||
if result.context_before:
|
||||
before = result.context_before[-1]
|
||||
|
||||
@@ -538,6 +538,11 @@ _APP_API_BLOCKLIST_METHOD_PATH = (
|
||||
# sidebar surfaces the session. Raw start works but the agent
|
||||
# fumbles the payload + the session doesn't reliably show up.
|
||||
("POST", "/api/research/start"),
|
||||
# Use web_search — the HTTP search route is UI-shaped and generic
|
||||
# app_api calls can return empty/poorly formatted results compared with the
|
||||
# named tool's source-aware output.
|
||||
("GET", "/api/search"),
|
||||
("POST", "/api/search"),
|
||||
# Use the named tools — they handle owner attribution, natural-
|
||||
# language due_date parsing, timezone, dedup, and tag/category
|
||||
# normalization. Hitting the raw endpoint via app_api saves a
|
||||
@@ -653,6 +658,8 @@ async def do_app_api(content: str, owner: Optional[str] = None) -> Dict:
|
||||
return {"error": "Don't POST /api/model/serve directly — use the `serve_model` or `serve_preset` tool (handles host resolution, env_prefix, and cookbook tracking).", "exit_code": 1}
|
||||
if "/api/research/start" in path:
|
||||
return {"error": "Don't POST /api/research/start directly — use the `trigger_research` tool (it surfaces the session in the Deep Research sidebar).", "exit_code": 1}
|
||||
if "/api/search" in path:
|
||||
return {"error": "Don't hit /api/search via app_api — use the `web_search` tool for online lookups, or `web_fetch` for a specific URL.", "exit_code": 1}
|
||||
if "/api/notes" in path:
|
||||
return {"error": "Don't hit /api/notes via app_api — use the `manage_notes` tool. It accepts natural-language due_date ('11pm today', 'tomorrow at 9am'), fires reminders from the due_date itself (no separate calendar event), and uses the caller's timezone. The raw endpoint requires ISO-UTC + a separate calendar event, both of which the agent tends to get wrong.", "exit_code": 1}
|
||||
if "/api/calendar/events" in path:
|
||||
|
||||
Reference in New Issue
Block a user