mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-08-05 02:45:28 +00:00
Merge 17beb95d10 into 20e7fc0164
This commit is contained in:
commit
a2b49fd0de
3 changed files with 143 additions and 1 deletions
|
|
@ -663,7 +663,11 @@ Generate an image. Line 1 = description, line 2 = model name, line 3 = WxH (e.g.
|
|||
"manage_session": "- ```manage_session``` — Rename, archive, delete, fork, switch, or `list` chats (the UI calls them 'chats'; 'session' is internal). Line 1 = action (list/switch/rename/archive/unarchive/delete/important/unimportant/truncate/fork), Line 2 = exact chat id from `list_sessions` (or `current` where supported). For delete/archive/truncate, always list first and reuse the exact id; never invent placeholder ids. `switch`/`open` returns a clickable anchor link the user can tap to open the chat — use for \"open my X chat\".",
|
||||
"manage_memory": "- ```manage_memory``` — Manage the user's persistent memory (facts about the USER themselves, their preferences, context that persists across chats). Line 1 = action (list/add/edit/delete/search), rest = content. Use when user says 'remember this' about themselves, states identity facts like 'my name is <name>' / 'call me <name>' / 'I live in <place>', or asks about stored memories. DO NOT use for info about another person (their address, phone, email, birthday) — that goes in `manage_contact`. If the user pastes an address/phone with a name and says 'save this for <person>', use `manage_contact add` with the address arg, NOT manage_memory.",
|
||||
"manage_skills": "- ```manage_skills``` — Skill registry (SKILL.md format). Args (JSON): {\"action\": \"list|view|view_ref|search|add|edit|patch|publish|delete\", ...}. `list` returns the index of available skills (published + teacher-escalation drafts); `view name=foo` fetches the full SKILL.md; `view_ref name=foo path=...` loads a reference file under the skill directory. For `add`, provide an explicit kebab-case `name` and only report the exact returned name, because storage may normalize or dedupe it. Use this BEFORE doing domain work — there may already be a procedure (published or draft) that prescribes the correct steps. Drafts written by the teacher loop are authoritative guidance even though they're not yet published.",
|
||||
"manage_tasks": "- ```manage_tasks``` — Create and manage scheduled background tasks (recurring AI jobs). Args (JSON): {\"action\": \"list|create|edit|delete|pause|resume|run\", ...}",
|
||||
"manage_tasks": """\
|
||||
```manage_tasks
|
||||
{"action": "create", "name": "<task name>", "task_type": "llm", "prompt": "<what to do>", "trigger_type": "schedule", "schedule": "daily", "scheduled_time": "09:00"}
|
||||
```
|
||||
Create and manage scheduled background tasks (recurring AI jobs). action=create needs `name`, `task_type` (llm/research/action), `prompt`, `trigger_type` (schedule/event), and `schedule` (once/daily/weekly/monthly). action=list returns all tasks. action=edit/delete/pause/resume/run needs `task_id`.""",
|
||||
"manage_endpoints": "- ```manage_endpoints``` — Add, remove, or configure AI model API endpoints. Args (JSON): {\"action\": \"list|add|delete|enable|disable\", ...}. Use when user wants to add a new AI provider.",
|
||||
"manage_mcp": "- ```manage_mcp``` — Manage MCP (Model Context Protocol) tool servers — external tools that extend your capabilities. Args (JSON): {\"action\": \"list|add|delete|reconnect|list_tools\", ...}",
|
||||
"manage_webhooks": "- ```manage_webhooks``` — Configure outgoing webhooks (HTTP notifications on events like chat completion). Args (JSON): {\"action\": \"list|add|delete|enable|disable\", ...}",
|
||||
|
|
|
|||
|
|
@ -282,8 +282,21 @@ async def do_manage_tasks(content: str, owner: Optional[str] = None) -> Dict:
|
|||
except ValueError:
|
||||
return {"error": "Invalid JSON arguments", "exit_code": 1}
|
||||
|
||||
# Normalize "add" -> "create": models that were not shown the enum sometimes
|
||||
# emit {"action": "add", ...} (same failure class as manage_skills #4013).
|
||||
if args.get("action") == "add":
|
||||
args["action"] = "create"
|
||||
if not args.get("action") and any(args.get(k) is not None for k in ("task", "description", "schedule", "time", "day_of_week")):
|
||||
args["action"] = "create"
|
||||
# Flatten a nested "task" object: {"action":"create","task":{"name":"foo",...}}
|
||||
# is equivalent to {"action":"create","name":"foo",...} but some models emit
|
||||
# the former. Hoist the inner fields so the rest of the handler sees them flat.
|
||||
task_obj = args.get("task")
|
||||
if isinstance(task_obj, dict):
|
||||
for _k, _v in task_obj.items():
|
||||
if args.get(_k) is None:
|
||||
args[_k] = _v
|
||||
args.pop("task", None)
|
||||
if args.get("task") and not args.get("name"):
|
||||
args["name"] = args["task"]
|
||||
if args.get("task") and not args.get("prompt"):
|
||||
|
|
@ -333,6 +346,11 @@ async def do_manage_tasks(content: str, owner: Optional[str] = None) -> Dict:
|
|||
task_type = args.get("task_type", "llm")
|
||||
trigger_type = args.get("trigger_type", "schedule")
|
||||
|
||||
# When the model omits "prompt" but supplies "name", use the name
|
||||
# as the prompt so a bare {"action":"create","name":"foo"} still
|
||||
# produces a runnable task instead of an error.
|
||||
if not args.get("prompt") and args.get("name"):
|
||||
args["prompt"] = args["name"]
|
||||
if task_type in ("llm", "research") and not args.get("prompt"):
|
||||
return {"error": "Prompt is required for llm/research tasks", "exit_code": 1}
|
||||
if task_type == "action" and not args.get("action_name"):
|
||||
|
|
|
|||
120
tests/test_manage_tasks_model_writes.py
Normal file
120
tests/test_manage_tasks_model_writes.py
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
"""Regression tests for manage_tasks model-write normalisation (#5757).
|
||||
|
||||
Models that were not shown the full tool schema (e.g. when RAG selects a
|
||||
different tool set or a local model is used without function-call schemas)
|
||||
sometimes emit action="add" instead of action="create", or wrap task fields in
|
||||
a nested "task" object rather than providing them at the top level. Before the
|
||||
fix, both patterns fell through to the "unknown action" error branch and the
|
||||
task was silently not created.
|
||||
|
||||
This test module verifies that both calling patterns produce a successfully
|
||||
created task, matching the same-class fix applied to manage_skills (#4013).
|
||||
"""
|
||||
|
||||
import json
|
||||
import tempfile
|
||||
|
||||
import pytest
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from sqlalchemy.pool import NullPool
|
||||
|
||||
from tests.helpers.import_state import clear_fake_database_modules
|
||||
|
||||
clear_fake_database_modules()
|
||||
|
||||
import core.database as cdb
|
||||
from core.database import ScheduledTask
|
||||
from src.tools.system import do_manage_tasks
|
||||
|
||||
_TMPDB = tempfile.NamedTemporaryFile(suffix=".db", delete=False)
|
||||
_ENGINE = create_engine(
|
||||
f"sqlite:///{_TMPDB.name}",
|
||||
connect_args={"check_same_thread": False},
|
||||
poolclass=NullPool,
|
||||
)
|
||||
cdb.Base.metadata.create_all(_ENGINE)
|
||||
_TS = sessionmaker(bind=_ENGINE, autoflush=False, autocommit=False)
|
||||
cdb.SessionLocal = _TS
|
||||
|
||||
|
||||
def _count_tasks_named(name):
|
||||
db = _TS()
|
||||
try:
|
||||
return db.query(ScheduledTask).filter(ScheduledTask.name == name).count()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_action_add_is_treated_as_create():
|
||||
"""action='add' must be normalised to 'create' so the task is persisted."""
|
||||
out = await do_manage_tasks(
|
||||
json.dumps({
|
||||
"action": "add",
|
||||
"name": "test task add",
|
||||
"prompt": "summarise the news",
|
||||
"task_type": "llm",
|
||||
"trigger_type": "schedule",
|
||||
"schedule": "daily",
|
||||
}),
|
||||
owner="alice",
|
||||
)
|
||||
assert out["exit_code"] == 0, out
|
||||
assert "Created task" in out["response"]
|
||||
assert _count_tasks_named("test task add") == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_nested_task_object_is_flattened():
|
||||
"""{'action':'add','task':{'name':'foo'}} must be treated as create with name=foo."""
|
||||
out = await do_manage_tasks(
|
||||
json.dumps({
|
||||
"action": "add",
|
||||
"task": {
|
||||
"name": "test task 2",
|
||||
},
|
||||
}),
|
||||
owner="alice",
|
||||
)
|
||||
assert out["exit_code"] == 0, out
|
||||
assert "Created task" in out["response"]
|
||||
assert _count_tasks_named("test task 2") == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_nested_task_object_with_prompt():
|
||||
"""Nested task object with explicit prompt field is handled correctly."""
|
||||
out = await do_manage_tasks(
|
||||
json.dumps({
|
||||
"action": "create",
|
||||
"task": {
|
||||
"name": "daily summary",
|
||||
"prompt": "summarise my emails",
|
||||
},
|
||||
"trigger_type": "schedule",
|
||||
"schedule": "daily",
|
||||
}),
|
||||
owner="alice",
|
||||
)
|
||||
assert out["exit_code"] == 0, out
|
||||
assert "Created task" in out["response"]
|
||||
assert _count_tasks_named("daily summary") == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_name_used_as_prompt_fallback_when_prompt_absent():
|
||||
"""When prompt is absent but name is present, name is used as the prompt."""
|
||||
out = await do_manage_tasks(
|
||||
json.dumps({
|
||||
"action": "create",
|
||||
"name": "my simple task",
|
||||
"task_type": "llm",
|
||||
"trigger_type": "schedule",
|
||||
"schedule": "daily",
|
||||
}),
|
||||
owner="alice",
|
||||
)
|
||||
assert out["exit_code"] == 0, out
|
||||
assert "Created task" in out["response"]
|
||||
assert _count_tasks_named("my simple task") == 1
|
||||
Loading…
Add table
Reference in a new issue