caudate-cli 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- api/__init__.py +5 -0
- api/anthropic_compat.py +1518 -0
- api/artifact_viewer.py +366 -0
- api/caudate_middleware.py +618 -0
- api/forge_bootstrapper_routes.py +377 -0
- api/forge_routes.py +630 -0
- api/forge_system_routes.py +294 -0
- api/openai_compat.py +1993 -0
- api/server.py +667 -0
- api/storyboard_page.py +677 -0
- caudate_cli-0.1.0.dist-info/METADATA +354 -0
- caudate_cli-0.1.0.dist-info/RECORD +153 -0
- caudate_cli-0.1.0.dist-info/WHEEL +5 -0
- caudate_cli-0.1.0.dist-info/entry_points.txt +2 -0
- caudate_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- caudate_cli-0.1.0.dist-info/top_level.txt +14 -0
- cognos_mcp/__init__.py +4 -0
- cognos_mcp/bridge.py +41 -0
- cognos_mcp/client.py +70 -0
- cognos_mcp/config.py +49 -0
- cognos_mcp/server.py +66 -0
- config.py +82 -0
- core/__init__.py +0 -0
- core/agent.py +468 -0
- core/agentic_loop.py +731 -0
- core/anthropic_auth.py +91 -0
- core/background.py +113 -0
- core/banner.py +134 -0
- core/bootstrap.py +292 -0
- core/citations.py +131 -0
- core/compaction.py +109 -0
- core/constitution.py +198 -0
- core/diff_viewer.py +87 -0
- core/export.py +85 -0
- core/file_refs.py +119 -0
- core/files.py +199 -0
- core/hooks.py +209 -0
- core/image.py +599 -0
- core/input.py +91 -0
- core/loop.py +238 -0
- core/memory_md.py +147 -0
- core/notifications.py +99 -0
- core/ownership.py +181 -0
- core/paste.py +81 -0
- core/permissions.py +210 -0
- core/plan_mode.py +215 -0
- core/sandbox_prompt.py +185 -0
- core/scheduler.py +195 -0
- core/schemas.py +202 -0
- core/session.py +90 -0
- core/settings.py +132 -0
- core/skills.py +398 -0
- core/slash_commands.py +977 -0
- core/statusline.py +61 -0
- core/subagent.py +300 -0
- core/thinking.py +50 -0
- core/updater.py +122 -0
- core/usage.py +109 -0
- core/worktree.py +93 -0
- execution/__init__.py +0 -0
- execution/executor.py +329 -0
- execution/plugins.py +108 -0
- execution/tools/__init__.py +0 -0
- execution/tools/agent_tool.py +107 -0
- execution/tools/agentic_tool.py +297 -0
- execution/tools/artifact_tool.py +191 -0
- execution/tools/ask_user_question_tool.py +137 -0
- execution/tools/base.py +81 -0
- execution/tools/calculator_tool.py +137 -0
- execution/tools/cognos_card_tool.py +124 -0
- execution/tools/cron_tool.py +215 -0
- execution/tools/datetime_tool.py +215 -0
- execution/tools/describe_image_tool.py +161 -0
- execution/tools/draw_tool.py +164 -0
- execution/tools/edit_image_tool.py +262 -0
- execution/tools/edit_tool.py +245 -0
- execution/tools/file_tool.py +90 -0
- execution/tools/find_anywhere_tool.py +255 -0
- execution/tools/forge_feature_tools.py +377 -0
- execution/tools/glob_tool.py +59 -0
- execution/tools/grep_tool.py +89 -0
- execution/tools/http_request_tool.py +224 -0
- execution/tools/load_skill_tool.py +104 -0
- execution/tools/longcat_avatar_tool.py +384 -0
- execution/tools/mcp_tool.py +100 -0
- execution/tools/notebook_tool.py +279 -0
- execution/tools/openapi_tool.py +440 -0
- execution/tools/plan_mode_tool.py +95 -0
- execution/tools/push_notification_tool.py +157 -0
- execution/tools/python_tool.py +61 -0
- execution/tools/respond_tool.py +40 -0
- execution/tools/sandbox_tool.py +378 -0
- execution/tools/search_tool.py +153 -0
- execution/tools/semantic_search_tool.py +106 -0
- execution/tools/shell_tool.py +283 -0
- execution/tools/speak_tool.py +134 -0
- execution/tools/storyboard_tool.py +727 -0
- execution/tools/system_info_tool.py +212 -0
- execution/tools/task_tool.py +323 -0
- execution/tools/think_tool.py +49 -0
- execution/tools/transcribe_audio_tool.py +86 -0
- execution/tools/update_memory_tool.py +92 -0
- execution/tools/web_fetch_tool.py +82 -0
- execution/tools/worktree_tool.py +174 -0
- llm/__init__.py +0 -0
- llm/fallback.py +116 -0
- llm/models.py +320 -0
- llm/provider.py +1356 -0
- llm/router.py +373 -0
- main.py +1889 -0
- memory/__init__.py +0 -0
- memory/episodic.py +99 -0
- memory/procedural.py +145 -0
- memory/semantic.py +71 -0
- memory/working.py +64 -0
- nn/__init__.py +43 -0
- nn/auto_evolve.py +245 -0
- nn/caudate.py +136 -0
- nn/config.py +141 -0
- nn/consolidator.py +81 -0
- nn/data.py +1635 -0
- nn/encoder.py +258 -0
- nn/forge_advisor.py +303 -0
- nn/format.py +235 -0
- nn/heads.py +432 -0
- nn/observer.py +994 -0
- nn/policy.py +214 -0
- nn/runtime.py +343 -0
- nn/scorer.py +175 -0
- nn/trainer.py +515 -0
- nn/vision.py +352 -0
- personality/__init__.py +23 -0
- personality/engine.py +129 -0
- personality/identity.py +144 -0
- personality/inner_voice.py +100 -0
- personality/mood.py +205 -0
- planning/__init__.py +0 -0
- planning/dev_server.py +221 -0
- planning/forge_models.py +718 -0
- planning/orchestrator.py +1363 -0
- planning/planner.py +451 -0
- planning/task_graph.py +61 -0
- reflection/__init__.py +0 -0
- reflection/meta_learner.py +156 -0
- reflection/reflector.py +127 -0
- ui/__init__.py +5 -0
- ui/display.py +88 -0
- voice/__init__.py +0 -0
- voice/conversation.py +125 -0
- voice/listener.py +111 -0
- voice/speaker.py +59 -0
- voice/stt.py +126 -0
- voice/tts.py +214 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"""SystemInfo — one-call summary of host hardware + software state.
|
|
2
|
+
|
|
3
|
+
Saves the LLM from chaining Bash calls (`uname`, `nvidia-smi`, `df`,
|
|
4
|
+
`free`, …) just to answer "what hardware do you have?". Intentionally
|
|
5
|
+
read-only; everything is via well-known commands.
|
|
6
|
+
|
|
7
|
+
The output is a structured text block that's easy for the LLM to
|
|
8
|
+
quote back to the user verbatim or summarise. Sections that are
|
|
9
|
+
unavailable (e.g. no NVIDIA GPU) are simply omitted.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import asyncio
|
|
15
|
+
import os
|
|
16
|
+
import platform
|
|
17
|
+
import shutil
|
|
18
|
+
import subprocess
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
from core.schemas import ToolResult
|
|
22
|
+
from execution.tools.base import BaseTool
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
_SECTION_TIMEOUT_S = 5 # per subprocess; SystemInfo isn't worth blocking on
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _run(cmd: list[str]) -> str:
|
|
29
|
+
"""Run a command and return stdout (or empty on failure)."""
|
|
30
|
+
try:
|
|
31
|
+
out = subprocess.run(
|
|
32
|
+
cmd, capture_output=True, text=True,
|
|
33
|
+
timeout=_SECTION_TIMEOUT_S,
|
|
34
|
+
)
|
|
35
|
+
return (out.stdout or out.stderr or "").strip()
|
|
36
|
+
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
37
|
+
return ""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _section_kernel() -> str:
|
|
41
|
+
bits = []
|
|
42
|
+
bits.append(f"OS: {platform.system()} {platform.release()}")
|
|
43
|
+
if platform.system() == "Linux":
|
|
44
|
+
# Distro name from /etc/os-release
|
|
45
|
+
os_release = ""
|
|
46
|
+
try:
|
|
47
|
+
with open("/etc/os-release") as f:
|
|
48
|
+
kv = dict(
|
|
49
|
+
line.strip().split("=", 1)
|
|
50
|
+
for line in f if "=" in line
|
|
51
|
+
)
|
|
52
|
+
os_release = kv.get("PRETTY_NAME", "").strip('"')
|
|
53
|
+
except OSError:
|
|
54
|
+
pass
|
|
55
|
+
if os_release:
|
|
56
|
+
bits.append(f"Distribution: {os_release}")
|
|
57
|
+
bits.append(f"Architecture: {platform.machine()}")
|
|
58
|
+
bits.append(f"Hostname: {platform.node()}")
|
|
59
|
+
return "\n".join(bits)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _section_cpu_ram() -> str:
|
|
63
|
+
bits = []
|
|
64
|
+
cpu_count = os.cpu_count() or 0
|
|
65
|
+
bits.append(f"CPUs: {cpu_count} logical")
|
|
66
|
+
# CPU model from /proc/cpuinfo on Linux
|
|
67
|
+
try:
|
|
68
|
+
with open("/proc/cpuinfo") as f:
|
|
69
|
+
for line in f:
|
|
70
|
+
if line.startswith("model name"):
|
|
71
|
+
bits.append(f"CPU model: {line.split(':',1)[1].strip()}")
|
|
72
|
+
break
|
|
73
|
+
except OSError:
|
|
74
|
+
pass
|
|
75
|
+
# Memory from /proc/meminfo
|
|
76
|
+
try:
|
|
77
|
+
mem = {}
|
|
78
|
+
with open("/proc/meminfo") as f:
|
|
79
|
+
for line in f:
|
|
80
|
+
k, _, v = line.partition(":")
|
|
81
|
+
mem[k.strip()] = v.strip()
|
|
82
|
+
total_kb = int(mem.get("MemTotal", "0 kB").split()[0] or "0")
|
|
83
|
+
avail_kb = int(mem.get("MemAvailable", "0 kB").split()[0] or "0")
|
|
84
|
+
gb = lambda x: x / 1024 / 1024
|
|
85
|
+
bits.append(
|
|
86
|
+
f"Memory: {gb(total_kb):.1f} GB total, "
|
|
87
|
+
f"{gb(avail_kb):.1f} GB available"
|
|
88
|
+
)
|
|
89
|
+
except (OSError, ValueError):
|
|
90
|
+
pass
|
|
91
|
+
return "\n".join(bits)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _section_gpu() -> str:
|
|
95
|
+
if not shutil.which("nvidia-smi"):
|
|
96
|
+
return "GPU: (no nvidia-smi found)"
|
|
97
|
+
raw = _run([
|
|
98
|
+
"nvidia-smi",
|
|
99
|
+
"--query-gpu=name,memory.total,memory.used,utilization.gpu,driver_version",
|
|
100
|
+
"--format=csv,noheader,nounits",
|
|
101
|
+
])
|
|
102
|
+
if not raw:
|
|
103
|
+
return "GPU: nvidia-smi present but no GPU info"
|
|
104
|
+
lines = ["GPUs:"]
|
|
105
|
+
for entry in raw.splitlines():
|
|
106
|
+
parts = [p.strip() for p in entry.split(",")]
|
|
107
|
+
if len(parts) < 5:
|
|
108
|
+
continue
|
|
109
|
+
name, mem_total, mem_used, util, driver = parts
|
|
110
|
+
lines.append(
|
|
111
|
+
f" - {name} ({mem_used}/{mem_total} MB used, "
|
|
112
|
+
f"{util}% util, driver {driver})"
|
|
113
|
+
)
|
|
114
|
+
return "\n".join(lines)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _section_disk() -> str:
|
|
118
|
+
raw = _run(["df", "-h", "--output=source,size,used,avail,pcent,target"])
|
|
119
|
+
if not raw:
|
|
120
|
+
return ""
|
|
121
|
+
lines = raw.splitlines()
|
|
122
|
+
# Filter to interesting mounts: skip squashfs, snap, tmpfs, etc.
|
|
123
|
+
keep = []
|
|
124
|
+
header = lines[0] if lines else ""
|
|
125
|
+
keep.append(header)
|
|
126
|
+
for ln in lines[1:]:
|
|
127
|
+
if any(k in ln for k in ("/snap/", "tmpfs", "udev", "squashfs", "overlay")):
|
|
128
|
+
continue
|
|
129
|
+
keep.append(ln)
|
|
130
|
+
return "Disks:\n" + "\n".join(f" {l}" for l in keep[:12])
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _section_python_cuda() -> str:
|
|
134
|
+
bits = [f"Python: {platform.python_version()}"]
|
|
135
|
+
# PyTorch + CUDA detection — best-effort, doesn't fail if torch missing
|
|
136
|
+
try:
|
|
137
|
+
import torch
|
|
138
|
+
bits.append(f"PyTorch: {torch.__version__}")
|
|
139
|
+
if torch.cuda.is_available():
|
|
140
|
+
bits.append(
|
|
141
|
+
f"CUDA: {torch.version.cuda} "
|
|
142
|
+
f"({torch.cuda.device_count()} device(s) visible to torch)"
|
|
143
|
+
)
|
|
144
|
+
else:
|
|
145
|
+
bits.append("CUDA: not available to torch")
|
|
146
|
+
except Exception as e:
|
|
147
|
+
bits.append(f"PyTorch: not importable ({e!s:.60})")
|
|
148
|
+
return "\n".join(bits)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _section_uptime_load() -> str:
|
|
152
|
+
bits = []
|
|
153
|
+
try:
|
|
154
|
+
with open("/proc/uptime") as f:
|
|
155
|
+
up = float(f.read().split()[0])
|
|
156
|
+
days, rem = divmod(up, 86400)
|
|
157
|
+
hours, rem = divmod(rem, 3600)
|
|
158
|
+
mins = rem // 60
|
|
159
|
+
bits.append(
|
|
160
|
+
f"Uptime: {int(days)}d {int(hours)}h {int(mins)}m"
|
|
161
|
+
)
|
|
162
|
+
except OSError:
|
|
163
|
+
pass
|
|
164
|
+
try:
|
|
165
|
+
with open("/proc/loadavg") as f:
|
|
166
|
+
la = f.read().strip().split()[:3]
|
|
167
|
+
bits.append(f"Load avg: {' / '.join(la)} (1/5/15 min)")
|
|
168
|
+
except OSError:
|
|
169
|
+
pass
|
|
170
|
+
return "\n".join(bits)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class SystemInfoTool(BaseTool):
|
|
174
|
+
name = "SystemInfo"
|
|
175
|
+
description = (
|
|
176
|
+
"Read-only one-call summary of the host machine: OS, CPU, RAM, "
|
|
177
|
+
"GPU (incl. NVIDIA), disk usage, Python/PyTorch/CUDA versions, "
|
|
178
|
+
"uptime. Use this when the user asks about the hardware/software "
|
|
179
|
+
"Cognos is running on, instead of chaining several Bash calls."
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
@property
|
|
183
|
+
def input_schema(self) -> dict[str, Any]:
|
|
184
|
+
return {"type": "object", "properties": {}, "required": []}
|
|
185
|
+
|
|
186
|
+
async def execute(self, **_: Any) -> ToolResult:
|
|
187
|
+
# Run sections in a thread so we don't block the event loop on
|
|
188
|
+
# subprocesses (nvidia-smi can take a second or two).
|
|
189
|
+
loop = asyncio.get_event_loop()
|
|
190
|
+
sections = await loop.run_in_executor(None, self._collect_all)
|
|
191
|
+
return ToolResult(
|
|
192
|
+
tool_name=self.name, status="success", output=sections
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
@staticmethod
|
|
196
|
+
def _collect_all() -> str:
|
|
197
|
+
parts: list[str] = []
|
|
198
|
+
for fn in (
|
|
199
|
+
_section_kernel,
|
|
200
|
+
_section_cpu_ram,
|
|
201
|
+
_section_gpu,
|
|
202
|
+
_section_disk,
|
|
203
|
+
_section_python_cuda,
|
|
204
|
+
_section_uptime_load,
|
|
205
|
+
):
|
|
206
|
+
try:
|
|
207
|
+
out = fn()
|
|
208
|
+
if out:
|
|
209
|
+
parts.append(out)
|
|
210
|
+
except Exception as e:
|
|
211
|
+
parts.append(f"({fn.__name__} failed: {e!s:.80})")
|
|
212
|
+
return "\n\n".join(parts)
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
"""Task tools — durable tracking for long-running async work.
|
|
2
|
+
|
|
3
|
+
Wraps `core/background.py::BackgroundTaskPool` so the LLM can spawn
|
|
4
|
+
fire-and-forget agent turns and check on them later. Useful when the
|
|
5
|
+
user asks for something that takes minutes (e.g. "generate a 12-panel
|
|
6
|
+
storyboard, I'll come back later") — the agent kicks off the work,
|
|
7
|
+
hands back a task id, and the next chat turn can `TaskGet` to surface
|
|
8
|
+
the result.
|
|
9
|
+
|
|
10
|
+
Five tools:
|
|
11
|
+
|
|
12
|
+
- **`TaskCreate`** — spawn a sub-prompt as a background task. The
|
|
13
|
+
agent runs the prompt in the same process; the user's chat turn
|
|
14
|
+
returns immediately with the task id.
|
|
15
|
+
- **`TaskList`** — show all tasks in the pool with status.
|
|
16
|
+
- **`TaskGet`** — fetch a single task by id (status, duration, result).
|
|
17
|
+
- **`TaskOutput`** — same as TaskGet but returns just the result text.
|
|
18
|
+
- **`TaskStop`** — cancel a running task by id.
|
|
19
|
+
|
|
20
|
+
Persistence: in-process only. When the Cognos server restarts, all
|
|
21
|
+
running tasks are lost (per the upstream BackgroundTaskPool design).
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import asyncio
|
|
27
|
+
import logging
|
|
28
|
+
from typing import Any
|
|
29
|
+
|
|
30
|
+
from core.background import BackgroundTask, get_global_pool
|
|
31
|
+
from core.schemas import ToolResult
|
|
32
|
+
from execution.tools.base import BaseTool
|
|
33
|
+
|
|
34
|
+
logger = logging.getLogger(__name__)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _format_task(t: BackgroundTask, *, brief: bool = False) -> str:
|
|
38
|
+
head = (
|
|
39
|
+
f" {t.id} [{t.status:>9}] duration={t.duration:6.1f}s\n"
|
|
40
|
+
f" label: {t.label[:80]}"
|
|
41
|
+
)
|
|
42
|
+
if brief:
|
|
43
|
+
return head
|
|
44
|
+
extra = []
|
|
45
|
+
if t.error:
|
|
46
|
+
extra.append(f" error: {t.error[:200]}")
|
|
47
|
+
if t.result:
|
|
48
|
+
extra.append(f" result (head): {t.result[:160]}")
|
|
49
|
+
return head + ("\n" + "\n".join(extra) if extra else "")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class TaskCreateTool(BaseTool):
|
|
53
|
+
mutates = True
|
|
54
|
+
name = "TaskCreate"
|
|
55
|
+
description = (
|
|
56
|
+
"Spawn a sub-prompt as a background task. The agent runs the "
|
|
57
|
+
"given `prompt` in the same Cognos process but doesn't block "
|
|
58
|
+
"the current chat turn — you get a task id back immediately. "
|
|
59
|
+
"Use this when the work will take longer than ~30 seconds and "
|
|
60
|
+
"the user doesn't want to wait. Use `TaskGet` / `TaskOutput` "
|
|
61
|
+
"later to fetch the result. Limitation: tasks run in-process, "
|
|
62
|
+
"so they die if the Cognos server restarts."
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
def __init__(self, agent: Any | None = None):
|
|
66
|
+
self._agent = agent
|
|
67
|
+
|
|
68
|
+
def set_agent(self, agent: Any) -> None:
|
|
69
|
+
self._agent = agent
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def input_schema(self) -> dict[str, Any]:
|
|
73
|
+
return {
|
|
74
|
+
"type": "object",
|
|
75
|
+
"properties": {
|
|
76
|
+
"prompt": {
|
|
77
|
+
"type": "string",
|
|
78
|
+
"description": "What the background agent turn should do.",
|
|
79
|
+
},
|
|
80
|
+
"label": {
|
|
81
|
+
"type": "string",
|
|
82
|
+
"description": "Short human-readable label shown in TaskList.",
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
"required": ["prompt"],
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async def execute(self, **kwargs: Any) -> ToolResult:
|
|
89
|
+
prompt = (kwargs.get("prompt") or "").strip()
|
|
90
|
+
label = (kwargs.get("label") or prompt[:60]).strip()
|
|
91
|
+
if not prompt:
|
|
92
|
+
return ToolResult(
|
|
93
|
+
tool_name=self.name, status="error",
|
|
94
|
+
error="`prompt` is required",
|
|
95
|
+
)
|
|
96
|
+
if self._agent is None or not hasattr(self._agent, "chat"):
|
|
97
|
+
return ToolResult(
|
|
98
|
+
tool_name=self.name, status="error",
|
|
99
|
+
error=(
|
|
100
|
+
"TaskCreate is not wired to a CognosAgent. The "
|
|
101
|
+
"agent must call set_agent() at startup."
|
|
102
|
+
),
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
pool = get_global_pool()
|
|
106
|
+
agent = self._agent
|
|
107
|
+
|
|
108
|
+
async def _run() -> str:
|
|
109
|
+
return await agent.chat(prompt)
|
|
110
|
+
|
|
111
|
+
bg = pool.submit(_run, label=label)
|
|
112
|
+
return ToolResult(
|
|
113
|
+
tool_name=self.name, status="success",
|
|
114
|
+
output=(
|
|
115
|
+
f"Background task started.\n"
|
|
116
|
+
f" task_id: {bg.id}\n"
|
|
117
|
+
f" label: {bg.label}\n"
|
|
118
|
+
f"Use TaskGet({bg.id!r}) or TaskOutput({bg.id!r}) to "
|
|
119
|
+
f"check on it. Use TaskStop({bg.id!r}) to cancel."
|
|
120
|
+
),
|
|
121
|
+
metadata={"task_id": bg.id, "label": bg.label, "status": bg.status},
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class TaskListTool(BaseTool):
|
|
126
|
+
name = "TaskList"
|
|
127
|
+
description = (
|
|
128
|
+
"List all background tasks in this process with their status, "
|
|
129
|
+
"duration, label, and (for finished ones) the result-head or "
|
|
130
|
+
"error. No arguments. Tasks are sorted newest-first."
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
@property
|
|
134
|
+
def input_schema(self) -> dict[str, Any]:
|
|
135
|
+
return {"type": "object", "properties": {}}
|
|
136
|
+
|
|
137
|
+
async def execute(self, **_: Any) -> ToolResult:
|
|
138
|
+
tasks = get_global_pool().list()
|
|
139
|
+
if not tasks:
|
|
140
|
+
return ToolResult(
|
|
141
|
+
tool_name=self.name, status="success",
|
|
142
|
+
output="No background tasks.",
|
|
143
|
+
metadata={"tasks": []},
|
|
144
|
+
)
|
|
145
|
+
lines = [f"{len(tasks)} background task(s):", ""]
|
|
146
|
+
lines.extend(_format_task(t) for t in tasks)
|
|
147
|
+
return ToolResult(
|
|
148
|
+
tool_name=self.name, status="success",
|
|
149
|
+
output="\n".join(lines),
|
|
150
|
+
metadata={
|
|
151
|
+
"tasks": [
|
|
152
|
+
{
|
|
153
|
+
"id": t.id,
|
|
154
|
+
"status": t.status,
|
|
155
|
+
"label": t.label,
|
|
156
|
+
"duration_s": t.duration,
|
|
157
|
+
"started_at": t.started_at,
|
|
158
|
+
"finished_at": t.finished_at or None,
|
|
159
|
+
"error": t.error,
|
|
160
|
+
}
|
|
161
|
+
for t in tasks
|
|
162
|
+
],
|
|
163
|
+
},
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class TaskGetTool(BaseTool):
|
|
168
|
+
name = "TaskGet"
|
|
169
|
+
description = (
|
|
170
|
+
"Fetch one background task by id. Returns its status, duration, "
|
|
171
|
+
"label, error (if any), and the full result text (if done). Use "
|
|
172
|
+
"this to check on work spawned via TaskCreate."
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
@property
|
|
176
|
+
def input_schema(self) -> dict[str, Any]:
|
|
177
|
+
return {
|
|
178
|
+
"type": "object",
|
|
179
|
+
"properties": {
|
|
180
|
+
"task_id": {
|
|
181
|
+
"type": "string",
|
|
182
|
+
"description": "Task id from TaskCreate / TaskList.",
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
"required": ["task_id"],
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async def execute(self, **kwargs: Any) -> ToolResult:
|
|
189
|
+
task_id = (kwargs.get("task_id") or "").strip()
|
|
190
|
+
if not task_id:
|
|
191
|
+
return ToolResult(
|
|
192
|
+
tool_name=self.name, status="error",
|
|
193
|
+
error="`task_id` is required",
|
|
194
|
+
)
|
|
195
|
+
bg = get_global_pool().get(task_id)
|
|
196
|
+
if bg is None:
|
|
197
|
+
return ToolResult(
|
|
198
|
+
tool_name=self.name, status="error",
|
|
199
|
+
error=f"no task with id {task_id!r}",
|
|
200
|
+
)
|
|
201
|
+
lines = [
|
|
202
|
+
f"task {bg.id}",
|
|
203
|
+
f" status: {bg.status}",
|
|
204
|
+
f" label: {bg.label}",
|
|
205
|
+
f" duration: {bg.duration:.1f}s",
|
|
206
|
+
]
|
|
207
|
+
if bg.error:
|
|
208
|
+
lines.append(f" error: {bg.error[:400]}")
|
|
209
|
+
if bg.result:
|
|
210
|
+
lines.append(f" result:")
|
|
211
|
+
lines.append(bg.result)
|
|
212
|
+
return ToolResult(
|
|
213
|
+
tool_name=self.name, status="success",
|
|
214
|
+
output="\n".join(lines),
|
|
215
|
+
metadata={
|
|
216
|
+
"id": bg.id,
|
|
217
|
+
"status": bg.status,
|
|
218
|
+
"label": bg.label,
|
|
219
|
+
"duration_s": bg.duration,
|
|
220
|
+
"error": bg.error,
|
|
221
|
+
"result": bg.result,
|
|
222
|
+
},
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class TaskOutputTool(BaseTool):
|
|
227
|
+
name = "TaskOutput"
|
|
228
|
+
description = (
|
|
229
|
+
"Return only the final output (result text) of a finished "
|
|
230
|
+
"background task. Errors if the task is still running, has "
|
|
231
|
+
"no output yet, or doesn't exist. Use this when you just "
|
|
232
|
+
"want the answer and don't care about metadata."
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
@property
|
|
236
|
+
def input_schema(self) -> dict[str, Any]:
|
|
237
|
+
return {
|
|
238
|
+
"type": "object",
|
|
239
|
+
"properties": {
|
|
240
|
+
"task_id": {
|
|
241
|
+
"type": "string",
|
|
242
|
+
"description": "Task id from TaskCreate / TaskList.",
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
"required": ["task_id"],
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async def execute(self, **kwargs: Any) -> ToolResult:
|
|
249
|
+
task_id = (kwargs.get("task_id") or "").strip()
|
|
250
|
+
if not task_id:
|
|
251
|
+
return ToolResult(
|
|
252
|
+
tool_name=self.name, status="error",
|
|
253
|
+
error="`task_id` is required",
|
|
254
|
+
)
|
|
255
|
+
bg = get_global_pool().get(task_id)
|
|
256
|
+
if bg is None:
|
|
257
|
+
return ToolResult(
|
|
258
|
+
tool_name=self.name, status="error",
|
|
259
|
+
error=f"no task with id {task_id!r}",
|
|
260
|
+
)
|
|
261
|
+
if bg.status in ("pending", "running"):
|
|
262
|
+
return ToolResult(
|
|
263
|
+
tool_name=self.name, status="error",
|
|
264
|
+
error=f"task {task_id} is still {bg.status} ({bg.duration:.1f}s elapsed)",
|
|
265
|
+
)
|
|
266
|
+
if bg.status == "failed":
|
|
267
|
+
return ToolResult(
|
|
268
|
+
tool_name=self.name, status="error",
|
|
269
|
+
error=f"task {task_id} failed: {bg.error}",
|
|
270
|
+
)
|
|
271
|
+
return ToolResult(
|
|
272
|
+
tool_name=self.name, status="success",
|
|
273
|
+
output=bg.result or "(empty)",
|
|
274
|
+
metadata={"id": bg.id, "status": bg.status},
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
class TaskStopTool(BaseTool):
|
|
279
|
+
mutates = True
|
|
280
|
+
name = "TaskStop"
|
|
281
|
+
description = (
|
|
282
|
+
"Cancel a running background task by id. No-op if the task is "
|
|
283
|
+
"already finished or doesn't exist. Returns success or 'not "
|
|
284
|
+
"cancellable'."
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
@property
|
|
288
|
+
def input_schema(self) -> dict[str, Any]:
|
|
289
|
+
return {
|
|
290
|
+
"type": "object",
|
|
291
|
+
"properties": {
|
|
292
|
+
"task_id": {
|
|
293
|
+
"type": "string",
|
|
294
|
+
"description": "Task id from TaskList.",
|
|
295
|
+
},
|
|
296
|
+
},
|
|
297
|
+
"required": ["task_id"],
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
async def execute(self, **kwargs: Any) -> ToolResult:
|
|
301
|
+
task_id = (kwargs.get("task_id") or "").strip()
|
|
302
|
+
if not task_id:
|
|
303
|
+
return ToolResult(
|
|
304
|
+
tool_name=self.name, status="error",
|
|
305
|
+
error="`task_id` is required",
|
|
306
|
+
)
|
|
307
|
+
ok = get_global_pool().cancel(task_id)
|
|
308
|
+
if not ok:
|
|
309
|
+
bg = get_global_pool().get(task_id)
|
|
310
|
+
if bg is None:
|
|
311
|
+
return ToolResult(
|
|
312
|
+
tool_name=self.name, status="error",
|
|
313
|
+
error=f"no task with id {task_id!r}",
|
|
314
|
+
)
|
|
315
|
+
return ToolResult(
|
|
316
|
+
tool_name=self.name, status="error",
|
|
317
|
+
error=f"task {task_id} is {bg.status} — not cancellable",
|
|
318
|
+
)
|
|
319
|
+
return ToolResult(
|
|
320
|
+
tool_name=self.name, status="success",
|
|
321
|
+
output=f"Cancellation requested for task {task_id}.",
|
|
322
|
+
metadata={"task_id": task_id},
|
|
323
|
+
)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Think tool — use the LLM to reason, generate knowledge, or synthesize information."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from core.schemas import ToolResult
|
|
8
|
+
from execution.tools.base import BaseTool
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ThinkTool(BaseTool):
|
|
12
|
+
name = "Think"
|
|
13
|
+
description = "Use the LLM to reason about a topic, answer a question, or synthesize information. Use this for knowledge questions, analysis, explanations, and any task that requires thinking rather than external tools."
|
|
14
|
+
|
|
15
|
+
@property
|
|
16
|
+
def input_schema(self) -> dict:
|
|
17
|
+
return {
|
|
18
|
+
"type": "object",
|
|
19
|
+
"properties": {
|
|
20
|
+
"question": {
|
|
21
|
+
"type": "string",
|
|
22
|
+
"description": "The question or topic to think about",
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
"required": ["question"],
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
def __init__(self, llm=None):
|
|
29
|
+
self._llm = llm
|
|
30
|
+
|
|
31
|
+
def set_llm(self, llm) -> None:
|
|
32
|
+
self._llm = llm
|
|
33
|
+
|
|
34
|
+
async def execute(self, **kwargs: Any) -> ToolResult:
|
|
35
|
+
question = kwargs.get("question", "")
|
|
36
|
+
if not question:
|
|
37
|
+
return self._error("No question provided")
|
|
38
|
+
|
|
39
|
+
if not self._llm:
|
|
40
|
+
return self._error("LLM not configured for think tool")
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
response = await self._llm.complete(
|
|
44
|
+
prompt=question,
|
|
45
|
+
system="You are a knowledgeable assistant. Provide clear, accurate, and helpful responses. Be thorough but concise.",
|
|
46
|
+
)
|
|
47
|
+
return self._success(response.content)
|
|
48
|
+
except Exception as e:
|
|
49
|
+
return self._error(f"Thinking failed: {e}")
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""TranscribeAudio — speech-to-text for a local audio file.
|
|
2
|
+
|
|
3
|
+
Uses Cognos's existing Whisper STT backend from `voice/stt.py`.
|
|
4
|
+
Loaded lazily on first call.
|
|
5
|
+
|
|
6
|
+
Accepts wav/mp3/flac/ogg/m4a — anything Whisper can read. Returns
|
|
7
|
+
the transcript as a single text block.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from core.schemas import ToolResult
|
|
17
|
+
from execution.tools.base import BaseTool
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class TranscribeAudioTool(BaseTool):
|
|
23
|
+
name = "TranscribeAudio"
|
|
24
|
+
description = (
|
|
25
|
+
"Convert a local audio file to text via Whisper. Use for "
|
|
26
|
+
"voice notes, meeting recordings, podcasts, voicemails, or "
|
|
27
|
+
"any 'what does this audio say?' request. Path must be a "
|
|
28
|
+
"local file (wav/mp3/flac/ogg/m4a/etc.). Returns the full "
|
|
29
|
+
"transcript."
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
def __init__(self):
|
|
33
|
+
self._stt: Any | None = None
|
|
34
|
+
|
|
35
|
+
def _get_stt(self):
|
|
36
|
+
if self._stt is None:
|
|
37
|
+
from voice.stt import make_stt
|
|
38
|
+
self._stt = make_stt(name="whisper")
|
|
39
|
+
return self._stt
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def input_schema(self) -> dict[str, Any]:
|
|
43
|
+
return {
|
|
44
|
+
"type": "object",
|
|
45
|
+
"properties": {
|
|
46
|
+
"path": {
|
|
47
|
+
"type": "string",
|
|
48
|
+
"description": "Path to a local audio file.",
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
"required": ["path"],
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async def execute(self, **kwargs: Any) -> ToolResult:
|
|
55
|
+
path = (kwargs.get("path") or "").strip()
|
|
56
|
+
p = Path(path).expanduser().resolve()
|
|
57
|
+
if not p.exists() or not p.is_file():
|
|
58
|
+
return ToolResult(
|
|
59
|
+
tool_name=self.name, status="error",
|
|
60
|
+
error=f"audio file not found: {p}",
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
audio_bytes = p.read_bytes()
|
|
65
|
+
stt = self._get_stt()
|
|
66
|
+
transcript = stt.transcribe(audio_bytes)
|
|
67
|
+
except Exception as e:
|
|
68
|
+
logger.exception("TranscribeAudio failed")
|
|
69
|
+
return ToolResult(
|
|
70
|
+
tool_name=self.name, status="error",
|
|
71
|
+
error=f"{type(e).__name__}: {e}",
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
if not transcript or not transcript.strip():
|
|
75
|
+
return ToolResult(
|
|
76
|
+
tool_name=self.name, status="success",
|
|
77
|
+
output="(empty transcript — no speech detected)",
|
|
78
|
+
metadata={"path": str(p), "transcript": ""},
|
|
79
|
+
)
|
|
80
|
+
return ToolResult(
|
|
81
|
+
tool_name=self.name, status="success",
|
|
82
|
+
output=transcript,
|
|
83
|
+
metadata={"path": str(p),
|
|
84
|
+
"transcript": transcript,
|
|
85
|
+
"char_count": len(transcript)},
|
|
86
|
+
)
|