mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-07 23:42:21 +02:00
Squash Odysseus development history
This commit is contained in:
+136
-40
@@ -77,6 +77,8 @@ async def do_manage_skills(content: str, owner: Optional[str] = None) -> Dict:
|
||||
if action == "view":
|
||||
if not name:
|
||||
return {"error": "name is required for view", "exit_code": 1}
|
||||
if args.get("path"):
|
||||
return {"error": "view reads SKILL.md only; use view_ref with name and path to read a supporting file.", "exit_code": 1}
|
||||
md = sm.read_skill_md(name, owner=owner)
|
||||
if md is None:
|
||||
return {"error": f"Skill {name!r} not found", "exit_code": 1}
|
||||
@@ -104,17 +106,11 @@ async def do_manage_skills(content: str, owner: Optional[str] = None) -> Dict:
|
||||
proc = args.get("steps") or []
|
||||
if not proc and not args.get("body_extra") and not args.get("solution"):
|
||||
return {"error": "procedure (or solution body) is required", "exit_code": 1}
|
||||
# Same auto-publish gate as the extractor path — when the user
|
||||
# has auto_approve_skills on and the caller didn't pin an explicit
|
||||
# status, publish immediately. Audit later demotes/removes on fail.
|
||||
# Newly learned procedures are always audited before they are allowed
|
||||
# into chat context. The automatic audit promotes passing skills.
|
||||
_status_arg = args.get("status")
|
||||
if not _status_arg:
|
||||
try:
|
||||
from routes.prefs_routes import _load_for_user as _load_prefs
|
||||
_prefs = _load_prefs(owner) or {}
|
||||
_status_arg = "published" if _prefs.get("auto_approve_skills", True) else "draft"
|
||||
except Exception:
|
||||
_status_arg = "draft"
|
||||
_status_arg = "draft"
|
||||
entry = sm.add_skill(
|
||||
name=args.get("name"),
|
||||
description=(args.get("description") or args.get("title") or "").strip(),
|
||||
@@ -162,7 +158,17 @@ async def do_manage_skills(content: str, owner: Optional[str] = None) -> Dict:
|
||||
return {"error": "name is required for edit", "exit_code": 1}
|
||||
new_content = args.get("content")
|
||||
if not isinstance(new_content, str) or not new_content.strip():
|
||||
return {"error": "content (full SKILL.md) is required for edit", "exit_code": 1}
|
||||
metadata_updates = {
|
||||
key: args[key] for key in (
|
||||
"description", "category", "when_to_use", "version", "confidence",
|
||||
"tags", "platforms", "requires_toolsets", "fallback_for_toolsets",
|
||||
"procedure", "pitfalls", "verification",
|
||||
) if key in args
|
||||
}
|
||||
if not metadata_updates:
|
||||
return {"error": "content (full SKILL.md) or an editable metadata field is required for edit", "exit_code": 1}
|
||||
ok = sm.update_skill(name, metadata_updates, owner=owner)
|
||||
return {"results": f"Edited skill `{name}`."} if ok else {"error": "Skill not found or update failed", "exit_code": 1}
|
||||
try:
|
||||
sk_new = Skill.from_markdown(new_content)
|
||||
except Exception as e:
|
||||
@@ -184,6 +190,8 @@ async def do_manage_skills(content: str, owner: Optional[str] = None) -> Dict:
|
||||
new_str = args.get("new_string", "")
|
||||
if not isinstance(old, str) or not old:
|
||||
return {"error": "old_string is required and must be non-empty", "exit_code": 1}
|
||||
if not isinstance(new_str, str):
|
||||
return {"error": "new_string must be a string; use an empty string to remove text", "exit_code": 1}
|
||||
md = sm.read_skill_md(name, owner=owner)
|
||||
if md is None:
|
||||
return {"error": f"Skill {name!r} not found", "exit_code": 1}
|
||||
@@ -211,8 +219,9 @@ async def do_manage_skills(content: str, owner: Optional[str] = None) -> Dict:
|
||||
updates = {"status": "published"}
|
||||
if args.get("confidence") is not None:
|
||||
updates["confidence"] = max(0.0, min(1.0, float(args["confidence"])))
|
||||
sm.update_skill(name, updates, owner=owner)
|
||||
return {"results": f"✅ Published `{name}`. It now appears in the skills index for future turns."}
|
||||
if not sm.update_skill(name, updates, owner=owner):
|
||||
return {"error": "Skill could not be published; no update was saved.", "exit_code": 1}
|
||||
return {"results": f"Published `{name}`. Automatic use remains subject to skill audit and approval settings."}
|
||||
|
||||
if action == "delete":
|
||||
if not name:
|
||||
@@ -271,6 +280,19 @@ def _skill_dump(sk) -> Dict:
|
||||
# Task management tool
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _task_date_utc(value):
|
||||
"""Parse the advertised one-off ISO datetime into the DB's naive UTC."""
|
||||
from datetime import datetime, timezone
|
||||
if not isinstance(value, str) or not value.strip():
|
||||
raise ValueError("scheduled_date is required for a one-off task")
|
||||
try:
|
||||
parsed = datetime.fromisoformat(value.strip().replace('Z', '+00:00'))
|
||||
except ValueError as exc:
|
||||
raise ValueError("scheduled_date must be an ISO datetime") from exc
|
||||
if parsed.tzinfo is not None:
|
||||
parsed = parsed.astimezone(timezone.utc).replace(tzinfo=None)
|
||||
return parsed
|
||||
|
||||
async def do_manage_tasks(content: str, owner: Optional[str] = None) -> Dict:
|
||||
"""Handle manage_tasks tool calls: CRUD on scheduled tasks."""
|
||||
import uuid as _uuid
|
||||
@@ -308,13 +330,79 @@ async def do_manage_tasks(content: str, owner: Optional[str] = None) -> Dict:
|
||||
args["scheduled_day"] = days[day]
|
||||
db = SessionLocal()
|
||||
try:
|
||||
def _task_by_id_or_exact_name(required_for: str):
|
||||
task_id = args.get("task_id")
|
||||
if task_id:
|
||||
task = db.query(ScheduledTask).filter(ScheduledTask.id == task_id).first()
|
||||
if task:
|
||||
return task, None
|
||||
|
||||
try:
|
||||
_uuid.UUID(str(task_id))
|
||||
looks_like_uuid = True
|
||||
except (TypeError, ValueError):
|
||||
looks_like_uuid = False
|
||||
if looks_like_uuid:
|
||||
return None, {"error": f"Task {task_id} not found", "exit_code": 1}
|
||||
|
||||
q = db.query(ScheduledTask).filter(ScheduledTask.name == str(task_id).strip())
|
||||
if owner:
|
||||
q = q.filter(ScheduledTask.owner == owner)
|
||||
matches = q.order_by(ScheduledTask.created_at.desc()).all()
|
||||
if len(matches) == 1:
|
||||
return matches[0], None
|
||||
if len(matches) > 1:
|
||||
return None, {
|
||||
"error": f"Task name '{task_id}' matched {len(matches)} tasks; use task_id",
|
||||
"exit_code": 1,
|
||||
}
|
||||
return None, {"error": f"Task {task_id} not found", "exit_code": 1}
|
||||
|
||||
name = str(args.get("name") or "").strip()
|
||||
if not name:
|
||||
return None, {"error": f"task_id is required for {required_for}", "exit_code": 1}
|
||||
|
||||
q = db.query(ScheduledTask).filter(ScheduledTask.name == name)
|
||||
if owner:
|
||||
q = q.filter(ScheduledTask.owner == owner)
|
||||
matches = q.order_by(ScheduledTask.created_at.desc()).all()
|
||||
if not matches:
|
||||
return None, {"error": f"Task named '{name}' not found", "exit_code": 1}
|
||||
if len(matches) > 1:
|
||||
return None, {
|
||||
"error": f"Task name '{name}' matched {len(matches)} tasks; use task_id",
|
||||
"exit_code": 1,
|
||||
}
|
||||
return matches[0], None
|
||||
|
||||
if action == "list":
|
||||
q = db.query(ScheduledTask)
|
||||
if owner:
|
||||
q = q.filter(ScheduledTask.owner == owner)
|
||||
status_filter = str(args.get("status") or "").strip().lower()
|
||||
if status_filter:
|
||||
q = q.filter(ScheduledTask.status == status_filter)
|
||||
name_filter = str(args.get("name") or "").strip()
|
||||
query_filter = str(
|
||||
args.get("query")
|
||||
or args.get("search")
|
||||
or args.get("pattern")
|
||||
or args.get("prompt")
|
||||
or args.get("match")
|
||||
or ""
|
||||
).strip()
|
||||
if name_filter:
|
||||
q = q.filter(ScheduledTask.name == name_filter)
|
||||
elif query_filter:
|
||||
from sqlalchemy import or_
|
||||
q = q.filter(or_(
|
||||
ScheduledTask.name.contains(query_filter),
|
||||
ScheduledTask.prompt.contains(query_filter),
|
||||
))
|
||||
tasks = q.order_by(ScheduledTask.created_at.desc()).all()
|
||||
if not tasks:
|
||||
return {"response": "No scheduled tasks found.", "exit_code": 0}
|
||||
suffix = f" matching '{name_filter or query_filter}'" if (name_filter or query_filter) else ""
|
||||
return {"response": f"No scheduled tasks found{suffix}.", "exit_code": 0}
|
||||
|
||||
lines = [f"Found {len(tasks)} tasks:"]
|
||||
for idx, t in enumerate(tasks, 1):
|
||||
@@ -326,6 +414,8 @@ async def do_manage_tasks(content: str, owner: Optional[str] = None) -> Dict:
|
||||
if t.next_run:
|
||||
bits.append(f"next {t.next_run.isoformat()}Z")
|
||||
detail = ", ".join(bits)
|
||||
if t.prompt:
|
||||
detail = f"{detail}; prompt: {t.prompt}"
|
||||
lines.append(f"{idx}. {t.name} ({t.id}) — {detail}")
|
||||
return {"response": "\n".join(lines), "exit_code": 0}
|
||||
|
||||
@@ -340,12 +430,17 @@ async def do_manage_tasks(content: str, owner: Optional[str] = None) -> Dict:
|
||||
|
||||
# Compute next_run for schedule triggers
|
||||
next_run = None
|
||||
scheduled_date = None
|
||||
if trigger_type == "schedule":
|
||||
schedule = args.get("schedule", "daily")
|
||||
if schedule == "once":
|
||||
scheduled_date = _task_date_utc(args.get("scheduled_date"))
|
||||
next_run = compute_next_run(
|
||||
schedule, args.get("scheduled_time", "09:00"),
|
||||
args.get("scheduled_day"),
|
||||
args.get("scheduled_day"), scheduled_date,
|
||||
)
|
||||
if schedule == "once" and next_run is None:
|
||||
return {"error": "scheduled_date must be in the future", "exit_code": 1}
|
||||
|
||||
task_id = str(_uuid.uuid4())
|
||||
# Guard each fallback with `or`: args.get("prompt", default) returns
|
||||
@@ -359,9 +454,10 @@ async def do_manage_tasks(content: str, owner: Optional[str] = None) -> Dict:
|
||||
prompt=args.get("prompt"),
|
||||
task_type=task_type,
|
||||
action=args.get("action_name"),
|
||||
schedule=args.get("schedule") if trigger_type == "schedule" else None,
|
||||
schedule=args.get("schedule", "daily") if trigger_type == "schedule" else None,
|
||||
scheduled_time=args.get("scheduled_time", "09:00") if trigger_type == "schedule" else None,
|
||||
scheduled_day=args.get("scheduled_day"),
|
||||
scheduled_date=scheduled_date,
|
||||
trigger_type=trigger_type,
|
||||
trigger_event=args.get("trigger_event"),
|
||||
trigger_count=args.get("trigger_count"),
|
||||
@@ -375,12 +471,9 @@ async def do_manage_tasks(content: str, owner: Optional[str] = None) -> Dict:
|
||||
return {"response": f"Created task '{name}' (id: {task_id})", "task_id": task_id, "exit_code": 0}
|
||||
|
||||
elif action == "edit":
|
||||
task_id = args.get("task_id")
|
||||
if not task_id:
|
||||
return {"error": "task_id is required for edit", "exit_code": 1}
|
||||
task = db.query(ScheduledTask).filter(ScheduledTask.id == task_id).first()
|
||||
if not task:
|
||||
return {"error": f"Task {task_id} not found", "exit_code": 1}
|
||||
task, error = _task_by_id_or_exact_name("edit")
|
||||
if error:
|
||||
return error
|
||||
# Strict ownership: the old `task.owner and task.owner != owner`
|
||||
# skipped the check on an owner-less task (created in no-login mode
|
||||
# or before the legacy-owner sweep), letting any authenticated user
|
||||
@@ -415,22 +508,28 @@ async def do_manage_tasks(content: str, owner: Optional[str] = None) -> Dict:
|
||||
setattr(task, field, args[field])
|
||||
changed.append(field)
|
||||
schedule_changed = True
|
||||
if "scheduled_date" in args:
|
||||
task.scheduled_date = _task_date_utc(args["scheduled_date"])
|
||||
changed.append("scheduled_date")
|
||||
schedule_changed = True
|
||||
|
||||
if schedule_changed and (task.trigger_type or "schedule") == "schedule":
|
||||
if task.schedule == "once" and task.scheduled_date is None:
|
||||
raise ValueError("scheduled_date is required for a one-off task")
|
||||
task.next_run = compute_next_run(
|
||||
task.schedule, task.scheduled_time, task.scheduled_day,
|
||||
task.scheduled_date,
|
||||
)
|
||||
if task.schedule == "once" and task.next_run is None:
|
||||
raise ValueError("scheduled_date must be in the future")
|
||||
|
||||
db.commit()
|
||||
return {"response": f"Updated task '{task.name}': {', '.join(changed)}", "exit_code": 0}
|
||||
|
||||
elif action == "delete":
|
||||
task_id = args.get("task_id")
|
||||
if not task_id:
|
||||
return {"error": "task_id is required for delete", "exit_code": 1}
|
||||
task = db.query(ScheduledTask).filter(ScheduledTask.id == task_id).first()
|
||||
if not task:
|
||||
return {"error": f"Task {task_id} not found", "exit_code": 1}
|
||||
task, error = _task_by_id_or_exact_name("delete")
|
||||
if error:
|
||||
return error
|
||||
if owner and task.owner != owner:
|
||||
return {"error": "Access denied", "exit_code": 1}
|
||||
name = task.name
|
||||
@@ -439,12 +538,9 @@ async def do_manage_tasks(content: str, owner: Optional[str] = None) -> Dict:
|
||||
return {"response": f"Deleted task '{name}'", "exit_code": 0}
|
||||
|
||||
elif action in ("pause", "resume"):
|
||||
task_id = args.get("task_id")
|
||||
if not task_id:
|
||||
return {"error": f"task_id is required for {action}", "exit_code": 1}
|
||||
task = db.query(ScheduledTask).filter(ScheduledTask.id == task_id).first()
|
||||
if not task:
|
||||
return {"error": f"Task {task_id} not found", "exit_code": 1}
|
||||
task, error = _task_by_id_or_exact_name(action)
|
||||
if error:
|
||||
return error
|
||||
if owner and task.owner != owner:
|
||||
return {"error": "Access denied", "exit_code": 1}
|
||||
|
||||
@@ -455,24 +551,24 @@ async def do_manage_tasks(content: str, owner: Optional[str] = None) -> Dict:
|
||||
if (task.trigger_type or "schedule") == "schedule":
|
||||
task.next_run = compute_next_run(
|
||||
task.schedule, task.scheduled_time, task.scheduled_day,
|
||||
task.scheduled_date,
|
||||
)
|
||||
if task.schedule == "once" and task.next_run is None:
|
||||
raise ValueError("A future scheduled_date is required to resume this one-off task")
|
||||
db.commit()
|
||||
return {"response": f"Task '{task.name}' {action}d", "exit_code": 0}
|
||||
|
||||
elif action == "run":
|
||||
task_id = args.get("task_id")
|
||||
if not task_id:
|
||||
return {"error": "task_id is required for run", "exit_code": 1}
|
||||
task = db.query(ScheduledTask).filter(ScheduledTask.id == task_id).first()
|
||||
if not task:
|
||||
return {"error": f"Task {task_id} not found", "exit_code": 1}
|
||||
task, error = _task_by_id_or_exact_name("run")
|
||||
if error:
|
||||
return error
|
||||
if owner and task.owner != owner:
|
||||
return {"error": "Access denied", "exit_code": 1}
|
||||
|
||||
from src.event_bus import get_task_scheduler
|
||||
scheduler = get_task_scheduler()
|
||||
if scheduler:
|
||||
started = await scheduler.run_task_now(task_id)
|
||||
started = await scheduler.run_task_now(task.id)
|
||||
if started:
|
||||
return {"response": f"Task '{task.name}' triggered", "exit_code": 0}
|
||||
else:
|
||||
|
||||
Reference in New Issue
Block a user