arkaos 2.2.2 → 2.3.1
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.
- package/VERSION +1 -1
- package/arka/skills/conclave/SKILL.md +194 -0
- package/arka/skills/human-writing/SKILL.md +143 -0
- package/config/agent-memory-template.md +28 -0
- package/config/disc-profiles.json +108 -0
- package/config/disc-team-validator.sh +94 -0
- package/config/gotchas-fixes.json +148 -0
- package/config/profile-template.json +12 -0
- package/config/providers-registry.json +56 -0
- package/config/settings-template.json +42 -0
- package/config/standards/communication.md +64 -0
- package/config/standards/orchestration.md +91 -0
- package/config/statusline-v2.sh +101 -0
- package/config/statusline.sh +139 -0
- package/config/system-prompt.sh +190 -0
- package/dashboard/LICENSE +21 -0
- package/dashboard/README.md +64 -0
- package/dashboard/app/app.config.ts +8 -0
- package/dashboard/app/app.vue +42 -0
- package/dashboard/app/assets/css/main.css +18 -0
- package/dashboard/app/composables/useApi.ts +8 -0
- package/dashboard/app/composables/useDashboard.ts +19 -0
- package/dashboard/app/error.vue +24 -0
- package/dashboard/app/layouts/default.vue +114 -0
- package/dashboard/app/pages/agents/[id].vue +506 -0
- package/dashboard/app/pages/agents/index.vue +225 -0
- package/dashboard/app/pages/budget.vue +132 -0
- package/dashboard/app/pages/commands.vue +180 -0
- package/dashboard/app/pages/health.vue +98 -0
- package/dashboard/app/pages/index.vue +126 -0
- package/dashboard/app/pages/knowledge.vue +729 -0
- package/dashboard/app/pages/personas.vue +597 -0
- package/dashboard/app/pages/settings.vue +146 -0
- package/dashboard/app/pages/tasks.vue +203 -0
- package/dashboard/app/types/index.d.ts +181 -0
- package/dashboard/app/utils/index.ts +7 -0
- package/dashboard/nuxt.config.ts +39 -0
- package/dashboard/package.json +37 -0
- package/dashboard/pnpm-workspace.yaml +7 -0
- package/dashboard/tsconfig.json +10 -0
- package/installer/cli.js +0 -0
- package/installer/index.js +262 -62
- package/knowledge/INDEX.md +34 -0
- package/knowledge/agents-registry.json +254 -0
- package/knowledge/channels-config.json +6 -0
- package/knowledge/commands-keywords.json +466 -0
- package/knowledge/commands-registry.json +2791 -0
- package/knowledge/commands-registry.json.bak +2791 -0
- package/knowledge/ecosystems.json +7 -0
- package/knowledge/obsidian-config.json +112 -0
- package/package.json +10 -6
- package/pyproject.toml +1 -1
- package/scripts/check-version.js +13 -0
- package/scripts/dashboard-api.py +636 -0
- package/scripts/knowledge-index.py +113 -0
- package/scripts/skill_validator.py +217 -0
- package/scripts/start-dashboard.sh +54 -0
- package/scripts/synapse-bridge.py +199 -0
- package/scripts/tools/brand_voice_analyzer.py +192 -0
- package/scripts/tools/dcf_calculator.py +168 -0
- package/scripts/tools/headline_scorer.py +215 -0
- package/scripts/tools/okr_cascade.py +207 -0
- package/scripts/tools/rice_prioritizer.py +230 -0
- package/scripts/tools/saas_metrics.py +234 -0
- package/scripts/tools/seo_checker.py +197 -0
- package/scripts/tools/tech_debt_analyzer.py +206 -0
|
@@ -0,0 +1,636 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""ArkaOS Dashboard API — FastAPI backend for the monitoring dashboard.
|
|
3
|
+
|
|
4
|
+
Exposes all ArkaOS data as REST endpoints consumed by the Nuxt 3 frontend.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
python scripts/dashboard-api.py # Start on :3334
|
|
8
|
+
python scripts/dashboard-api.py --port 8000 # Custom port
|
|
9
|
+
python scripts/dashboard-api.py --host 0.0.0.0 # Allow external access
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import subprocess
|
|
15
|
+
import sys
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Optional
|
|
18
|
+
|
|
19
|
+
# Resolve ArkaOS root
|
|
20
|
+
ARKAOS_ROOT = Path(os.environ.get("ARKAOS_ROOT", Path(__file__).resolve().parent.parent))
|
|
21
|
+
sys.path.insert(0, str(ARKAOS_ROOT))
|
|
22
|
+
|
|
23
|
+
from fastapi import FastAPI, Query, WebSocket, WebSocketDisconnect
|
|
24
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
25
|
+
|
|
26
|
+
app = FastAPI(title="ArkaOS Dashboard API", version="2.2.0")
|
|
27
|
+
|
|
28
|
+
# --- WebSocket — thread-safe message queue ---
|
|
29
|
+
import asyncio
|
|
30
|
+
import queue as _queue
|
|
31
|
+
|
|
32
|
+
_ws_clients: list[WebSocket] = []
|
|
33
|
+
_ws_message_queue: _queue.Queue = _queue.Queue()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def broadcast_from_thread(data: dict):
|
|
37
|
+
"""Thread-safe: put message in queue, WebSocket loop picks it up."""
|
|
38
|
+
_ws_message_queue.put(data)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@app.websocket("/ws/tasks")
|
|
42
|
+
async def ws_tasks(websocket: WebSocket):
|
|
43
|
+
await websocket.accept()
|
|
44
|
+
_ws_clients.append(websocket)
|
|
45
|
+
try:
|
|
46
|
+
while True:
|
|
47
|
+
# Check message queue every 100ms
|
|
48
|
+
try:
|
|
49
|
+
while not _ws_message_queue.empty():
|
|
50
|
+
msg = _ws_message_queue.get_nowait()
|
|
51
|
+
dead = []
|
|
52
|
+
for client in _ws_clients:
|
|
53
|
+
try:
|
|
54
|
+
await client.send_json(msg)
|
|
55
|
+
except Exception:
|
|
56
|
+
dead.append(client)
|
|
57
|
+
for d in dead:
|
|
58
|
+
if d in _ws_clients:
|
|
59
|
+
_ws_clients.remove(d)
|
|
60
|
+
except _queue.Empty:
|
|
61
|
+
pass
|
|
62
|
+
await asyncio.sleep(0.1)
|
|
63
|
+
except WebSocketDisconnect:
|
|
64
|
+
if websocket in _ws_clients:
|
|
65
|
+
_ws_clients.remove(websocket)
|
|
66
|
+
|
|
67
|
+
app.add_middleware(
|
|
68
|
+
CORSMiddleware,
|
|
69
|
+
allow_origins=["http://localhost:3333", "http://localhost:3000"],
|
|
70
|
+
allow_methods=["GET", "POST", "PUT", "DELETE"],
|
|
71
|
+
allow_headers=["*"],
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# --- Data loaders (lazy, cached) ---
|
|
75
|
+
|
|
76
|
+
_agents_cache = None
|
|
77
|
+
_commands_cache = None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _load_agents() -> list[dict]:
|
|
81
|
+
global _agents_cache
|
|
82
|
+
if _agents_cache is None:
|
|
83
|
+
path = ARKAOS_ROOT / "knowledge" / "agents-registry-v2.json"
|
|
84
|
+
if path.exists():
|
|
85
|
+
data = json.loads(path.read_text())
|
|
86
|
+
_agents_cache = data.get("agents", [])
|
|
87
|
+
else:
|
|
88
|
+
_agents_cache = []
|
|
89
|
+
return _agents_cache
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _load_commands() -> list[dict]:
|
|
93
|
+
global _commands_cache
|
|
94
|
+
if _commands_cache is None:
|
|
95
|
+
path = ARKAOS_ROOT / "knowledge" / "commands-registry-v2.json"
|
|
96
|
+
if path.exists():
|
|
97
|
+
data = json.loads(path.read_text())
|
|
98
|
+
_commands_cache = data.get("commands", [])
|
|
99
|
+
else:
|
|
100
|
+
_commands_cache = []
|
|
101
|
+
return _commands_cache
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _get_budget_manager():
|
|
105
|
+
try:
|
|
106
|
+
from core.budget.manager import BudgetManager
|
|
107
|
+
db_path = Path.home() / ".arkaos" / "budget-usage.json"
|
|
108
|
+
return BudgetManager(storage_path=db_path)
|
|
109
|
+
except Exception:
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _get_task_manager():
|
|
114
|
+
try:
|
|
115
|
+
from core.tasks.manager import TaskManager
|
|
116
|
+
db_path = Path.home() / ".arkaos" / "tasks.json"
|
|
117
|
+
return TaskManager(storage_path=db_path)
|
|
118
|
+
except Exception:
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _get_vector_store():
|
|
123
|
+
try:
|
|
124
|
+
from core.knowledge.vector_store import VectorStore
|
|
125
|
+
db_path = Path.home() / ".arkaos" / "knowledge.db"
|
|
126
|
+
if db_path.exists():
|
|
127
|
+
return VectorStore(db_path)
|
|
128
|
+
except Exception:
|
|
129
|
+
pass
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
# --- Endpoints ---
|
|
134
|
+
|
|
135
|
+
@app.get("/api/overview")
|
|
136
|
+
def overview():
|
|
137
|
+
agents = _load_agents()
|
|
138
|
+
commands = _load_commands()
|
|
139
|
+
departments = set(a.get("department", "") for a in agents)
|
|
140
|
+
|
|
141
|
+
skills_count = 0
|
|
142
|
+
try:
|
|
143
|
+
skills_count = int(subprocess.run(
|
|
144
|
+
["find", str(ARKAOS_ROOT / "departments"), "-name", "SKILL.md", "-path", "*/skills/*/SKILL.md"],
|
|
145
|
+
capture_output=True, text=True, timeout=5,
|
|
146
|
+
).stdout.strip().count("\n")) + 1
|
|
147
|
+
except Exception:
|
|
148
|
+
skills_count = 250
|
|
149
|
+
|
|
150
|
+
budget_mgr = _get_budget_manager()
|
|
151
|
+
task_mgr = _get_task_manager()
|
|
152
|
+
store = _get_vector_store()
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
"agents": len(agents),
|
|
156
|
+
"commands": len(commands),
|
|
157
|
+
"departments": len(departments),
|
|
158
|
+
"skills": skills_count,
|
|
159
|
+
"workflows": 24,
|
|
160
|
+
"tests": 1809,
|
|
161
|
+
"version": "2.0.3",
|
|
162
|
+
"budget": budget_mgr.get_summary(tier=2).model_dump() if budget_mgr else None,
|
|
163
|
+
"tasks": task_mgr.summary() if task_mgr else {"total": 0, "active": 0, "queued": 0},
|
|
164
|
+
"knowledge": store.get_stats() if store else {"total_chunks": 0, "total_files": 0},
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@app.get("/api/agents")
|
|
169
|
+
def agents(dept: Optional[str] = Query(None)):
|
|
170
|
+
data = _load_agents()
|
|
171
|
+
if dept:
|
|
172
|
+
data = [a for a in data if a.get("department") == dept]
|
|
173
|
+
return {"agents": data, "total": len(data)}
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@app.get("/api/agents/{agent_id}")
|
|
177
|
+
def agent_detail(agent_id: str):
|
|
178
|
+
"""Get full agent detail including YAML data."""
|
|
179
|
+
# First get registry data
|
|
180
|
+
agents = _load_agents()
|
|
181
|
+
base = None
|
|
182
|
+
for a in agents:
|
|
183
|
+
if a.get("id") == agent_id:
|
|
184
|
+
base = dict(a)
|
|
185
|
+
break
|
|
186
|
+
if not base:
|
|
187
|
+
return {"error": "Agent not found"}
|
|
188
|
+
|
|
189
|
+
# Enrich with full YAML data
|
|
190
|
+
yaml_file = ARKAOS_ROOT / base.get("file", "")
|
|
191
|
+
if yaml_file.exists():
|
|
192
|
+
try:
|
|
193
|
+
import yaml
|
|
194
|
+
raw = yaml.safe_load(yaml_file.read_text())
|
|
195
|
+
dna = raw.get("behavioral_dna", {})
|
|
196
|
+
disc = dna.get("disc", {})
|
|
197
|
+
ennea = dna.get("enneagram", {})
|
|
198
|
+
|
|
199
|
+
base["disc"]["communication_style"] = disc.get("communication_style", "")
|
|
200
|
+
base["disc"]["under_pressure"] = disc.get("under_pressure", "")
|
|
201
|
+
base["disc"]["motivator"] = disc.get("motivator", "")
|
|
202
|
+
base["enneagram"]["core_motivation"] = ennea.get("core_motivation", "")
|
|
203
|
+
base["enneagram"]["core_fear"] = ennea.get("core_fear", "")
|
|
204
|
+
base["enneagram"]["subtype"] = ennea.get("subtype", "")
|
|
205
|
+
|
|
206
|
+
base["mental_models"] = raw.get("mental_models", {})
|
|
207
|
+
base["communication"] = raw.get("communication", {})
|
|
208
|
+
|
|
209
|
+
auth = raw.get("authority", {})
|
|
210
|
+
base["authority"]["delegates_to"] = auth.get("delegates_to", [])
|
|
211
|
+
base["authority"]["escalates_to"] = auth.get("escalates_to", "")
|
|
212
|
+
|
|
213
|
+
expertise = raw.get("expertise", {})
|
|
214
|
+
base["expertise_depth"] = expertise.get("depth", "")
|
|
215
|
+
base["expertise_years"] = expertise.get("years_equivalent", 0)
|
|
216
|
+
except Exception:
|
|
217
|
+
pass
|
|
218
|
+
|
|
219
|
+
return base
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
@app.get("/api/commands")
|
|
223
|
+
def commands(dept: Optional[str] = Query(None), q: Optional[str] = Query(None)):
|
|
224
|
+
data = _load_commands()
|
|
225
|
+
if dept:
|
|
226
|
+
data = [c for c in data if c.get("department") == dept]
|
|
227
|
+
if q:
|
|
228
|
+
q_lower = q.lower()
|
|
229
|
+
data = [c for c in data if q_lower in c.get("command", "").lower() or q_lower in c.get("description", "").lower()]
|
|
230
|
+
return {"commands": data, "total": len(data)}
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
@app.get("/api/budget")
|
|
234
|
+
def budget_all():
|
|
235
|
+
mgr = _get_budget_manager()
|
|
236
|
+
if not mgr:
|
|
237
|
+
return {"tiers": [], "departments": [], "summary": {"total_tokens": 0, "total_ops": 0, "active_departments": 0}}
|
|
238
|
+
|
|
239
|
+
# Department breakdown from raw usages
|
|
240
|
+
dept_data: dict[str, dict] = {}
|
|
241
|
+
for u in mgr._usages:
|
|
242
|
+
dept = u.department or "system"
|
|
243
|
+
if dept not in dept_data:
|
|
244
|
+
dept_data[dept] = {"department": dept, "tokens": 0, "operations": 0}
|
|
245
|
+
dept_data[dept]["tokens"] += u.tokens
|
|
246
|
+
dept_data[dept]["operations"] += 1
|
|
247
|
+
|
|
248
|
+
departments = sorted(dept_data.values(), key=lambda d: d["tokens"], reverse=True)
|
|
249
|
+
max_tokens = departments[0]["tokens"] if departments else 1
|
|
250
|
+
|
|
251
|
+
# Add relative percentage for bar width
|
|
252
|
+
for d in departments:
|
|
253
|
+
d["percent"] = round((d["tokens"] / max_tokens) * 100) if max_tokens > 0 else 0
|
|
254
|
+
|
|
255
|
+
total_tokens = sum(d["tokens"] for d in departments)
|
|
256
|
+
total_ops = sum(d["operations"] for d in departments)
|
|
257
|
+
|
|
258
|
+
return {
|
|
259
|
+
"summary": {
|
|
260
|
+
"total_tokens": total_tokens,
|
|
261
|
+
"total_ops": total_ops,
|
|
262
|
+
"active_departments": len(departments),
|
|
263
|
+
"estimated_cost_usd": round(total_tokens * 0.000003, 4), # ~$3 per 1M input tokens
|
|
264
|
+
},
|
|
265
|
+
"departments": departments,
|
|
266
|
+
"tiers": [mgr.get_summary(tier=t).model_dump() for t in range(4)],
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
@app.get("/api/budget/{tier}")
|
|
271
|
+
def budget_tier(tier: int):
|
|
272
|
+
mgr = _get_budget_manager()
|
|
273
|
+
if not mgr:
|
|
274
|
+
return {"error": "Budget manager unavailable"}
|
|
275
|
+
return mgr.get_summary(tier=tier).model_dump()
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
@app.get("/api/tasks")
|
|
279
|
+
def tasks(status: Optional[str] = Query(None)):
|
|
280
|
+
mgr = _get_task_manager()
|
|
281
|
+
if not mgr:
|
|
282
|
+
return {"tasks": [], "summary": {"total": 0}}
|
|
283
|
+
from core.tasks.schema import TaskStatus
|
|
284
|
+
task_list = mgr.list_all(TaskStatus(status) if status else None)
|
|
285
|
+
return {
|
|
286
|
+
"tasks": [t.model_dump() for t in task_list],
|
|
287
|
+
"summary": mgr.summary(),
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
@app.get("/api/tasks/active")
|
|
292
|
+
def tasks_active():
|
|
293
|
+
mgr = _get_task_manager()
|
|
294
|
+
if not mgr:
|
|
295
|
+
return {"tasks": []}
|
|
296
|
+
return {"tasks": [t.model_dump() for t in mgr.list_active()]}
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
# --- Job Queue (SQLite) ---
|
|
300
|
+
|
|
301
|
+
_job_manager = None
|
|
302
|
+
|
|
303
|
+
def _get_job_manager():
|
|
304
|
+
global _job_manager
|
|
305
|
+
if _job_manager is None:
|
|
306
|
+
try:
|
|
307
|
+
from core.jobs.manager import JobManager
|
|
308
|
+
_job_manager = JobManager()
|
|
309
|
+
except Exception:
|
|
310
|
+
return None
|
|
311
|
+
return _job_manager
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
@app.get("/api/jobs")
|
|
315
|
+
def jobs_list(status: Optional[str] = Query(None), limit: int = Query(50)):
|
|
316
|
+
mgr = _get_job_manager()
|
|
317
|
+
if not mgr:
|
|
318
|
+
return {"jobs": [], "summary": {}}
|
|
319
|
+
if status:
|
|
320
|
+
jobs = mgr.list_by_status(status, limit)
|
|
321
|
+
else:
|
|
322
|
+
jobs = mgr.list_all(limit)
|
|
323
|
+
return {"jobs": [j.to_dict() for j in jobs], "summary": mgr.summary()}
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
@app.get("/api/jobs/{job_id}")
|
|
327
|
+
def job_detail(job_id: str):
|
|
328
|
+
mgr = _get_job_manager()
|
|
329
|
+
if not mgr:
|
|
330
|
+
return {"error": "Job manager unavailable"}
|
|
331
|
+
job = mgr.get(job_id)
|
|
332
|
+
if not job:
|
|
333
|
+
return {"error": "Job not found"}
|
|
334
|
+
return job.to_dict()
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
@app.delete("/api/jobs/{job_id}")
|
|
338
|
+
def job_cancel(job_id: str):
|
|
339
|
+
mgr = _get_job_manager()
|
|
340
|
+
if not mgr:
|
|
341
|
+
return {"error": "Job manager unavailable"}
|
|
342
|
+
if mgr.cancel(job_id):
|
|
343
|
+
broadcast_from_thread({"type": "job_cancelled", "job_id": job_id})
|
|
344
|
+
return {"cancelled": True}
|
|
345
|
+
return {"error": "Can only cancel queued jobs"}
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
@app.post("/api/knowledge/ingest")
|
|
349
|
+
def knowledge_ingest(body: dict):
|
|
350
|
+
"""Ingest content into the knowledge base. Runs in background with SQLite job tracking."""
|
|
351
|
+
import threading
|
|
352
|
+
|
|
353
|
+
source = body.get("source", "")
|
|
354
|
+
source_type = body.get("type", "")
|
|
355
|
+
if not source:
|
|
356
|
+
return {"error": "source is required"}
|
|
357
|
+
|
|
358
|
+
store = _get_vector_store()
|
|
359
|
+
if not store:
|
|
360
|
+
from core.knowledge.vector_store import VectorStore
|
|
361
|
+
kb_db = Path.home() / ".arkaos" / "knowledge.db"
|
|
362
|
+
kb_db.parent.mkdir(parents=True, exist_ok=True)
|
|
363
|
+
store = VectorStore(kb_db)
|
|
364
|
+
|
|
365
|
+
from core.knowledge.ingest import IngestEngine, detect_source_type
|
|
366
|
+
if not source_type:
|
|
367
|
+
source_type = detect_source_type(source)
|
|
368
|
+
|
|
369
|
+
# Create job in SQLite
|
|
370
|
+
job_mgr = _get_job_manager()
|
|
371
|
+
job = job_mgr.create(source=source, source_type=source_type)
|
|
372
|
+
|
|
373
|
+
job_id = job.id # Capture ID for thread
|
|
374
|
+
|
|
375
|
+
def run_ingest():
|
|
376
|
+
# Create thread-local JobManager (SQLite objects can't cross threads)
|
|
377
|
+
from core.jobs.manager import JobManager as _JM
|
|
378
|
+
local_mgr = _JM()
|
|
379
|
+
|
|
380
|
+
engine = IngestEngine(store)
|
|
381
|
+
def on_progress(pct, msg):
|
|
382
|
+
status = "processing"
|
|
383
|
+
if "phase 2" in msg.lower() or "download" in msg.lower():
|
|
384
|
+
status = "downloading"
|
|
385
|
+
elif "phase 3" in msg.lower() or "extract" in msg.lower():
|
|
386
|
+
status = "processing"
|
|
387
|
+
elif "phase 4" in msg.lower() or "transcrib" in msg.lower():
|
|
388
|
+
status = "transcribing"
|
|
389
|
+
elif "embed" in msg.lower() or "index" in msg.lower():
|
|
390
|
+
status = "embedding"
|
|
391
|
+
local_mgr.update_progress(job_id, pct, msg, status)
|
|
392
|
+
broadcast_from_thread({
|
|
393
|
+
"type": "job_progress",
|
|
394
|
+
"job_id": job_id,
|
|
395
|
+
"progress": pct,
|
|
396
|
+
"message": msg,
|
|
397
|
+
"status": status,
|
|
398
|
+
})
|
|
399
|
+
try:
|
|
400
|
+
local_mgr.start(job_id)
|
|
401
|
+
broadcast_from_thread({"type": "job_progress", "job_id": job_id, "progress": 0, "message": "Starting...", "status": "processing"})
|
|
402
|
+
result = engine.ingest(source, source_type, on_progress=on_progress)
|
|
403
|
+
if result.success:
|
|
404
|
+
local_mgr.complete(job_id, chunks_created=result.chunks_created)
|
|
405
|
+
broadcast_from_thread({"type": "job_complete", "job_id": job_id, "chunks_created": result.chunks_created})
|
|
406
|
+
else:
|
|
407
|
+
local_mgr.fail(job_id, result.error)
|
|
408
|
+
broadcast_from_thread({"type": "job_failed", "job_id": job_id, "error": result.error})
|
|
409
|
+
except Exception as e:
|
|
410
|
+
local_mgr.fail(job_id, str(e))
|
|
411
|
+
broadcast_from_thread({"type": "job_failed", "job_id": job_id, "error": str(e)})
|
|
412
|
+
|
|
413
|
+
thread = threading.Thread(target=run_ingest, daemon=True)
|
|
414
|
+
thread.start()
|
|
415
|
+
|
|
416
|
+
return {"job_id": job.id, "source_type": source_type, "status": "queued"}
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
@app.get("/api/tasks/{task_id}")
|
|
420
|
+
def task_detail(task_id: str):
|
|
421
|
+
"""Get a single task by ID. Also checks jobs."""
|
|
422
|
+
# Check jobs first (new system)
|
|
423
|
+
job_mgr = _get_job_manager()
|
|
424
|
+
if job_mgr:
|
|
425
|
+
job = job_mgr.get(task_id)
|
|
426
|
+
if job:
|
|
427
|
+
return job.to_dict()
|
|
428
|
+
# Fallback to old task manager
|
|
429
|
+
mgr = _get_task_manager()
|
|
430
|
+
if not mgr:
|
|
431
|
+
return {"error": "Task manager unavailable"}
|
|
432
|
+
task = mgr.get(task_id)
|
|
433
|
+
if not task:
|
|
434
|
+
return {"error": "Task not found"}
|
|
435
|
+
return task.model_dump()
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
@app.get("/api/knowledge/stats")
|
|
439
|
+
def knowledge_stats():
|
|
440
|
+
store = _get_vector_store()
|
|
441
|
+
if not store:
|
|
442
|
+
return {"total_chunks": 0, "total_files": 0, "vss_available": False, "indexed": False}
|
|
443
|
+
stats = store.get_stats()
|
|
444
|
+
stats["indexed"] = stats["total_chunks"] > 0
|
|
445
|
+
return stats
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
@app.get("/api/knowledge/search")
|
|
449
|
+
def knowledge_search(q: str = Query(...), top_k: int = Query(5)):
|
|
450
|
+
store = _get_vector_store()
|
|
451
|
+
if not store:
|
|
452
|
+
return {"results": [], "query": q}
|
|
453
|
+
results = store.search(q, top_k=min(top_k, 20))
|
|
454
|
+
return {"results": results, "query": q, "total": len(results)}
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
@app.get("/api/health")
|
|
458
|
+
def health():
|
|
459
|
+
checks = []
|
|
460
|
+
arkaos_home = Path.home() / ".arkaos"
|
|
461
|
+
|
|
462
|
+
def check(name, condition, fix=""):
|
|
463
|
+
checks.append({"name": name, "passed": condition, "fix": fix})
|
|
464
|
+
|
|
465
|
+
check("install_dir", arkaos_home.exists(), "Run: npx arkaos install")
|
|
466
|
+
check("manifest", (arkaos_home / "install-manifest.json").exists(), "Run: npx arkaos install")
|
|
467
|
+
check("constitution", (ARKAOS_ROOT / "config" / "constitution.yaml").exists())
|
|
468
|
+
check("agents_registry", (ARKAOS_ROOT / "knowledge" / "agents-registry-v2.json").exists())
|
|
469
|
+
check("commands_registry", (ARKAOS_ROOT / "knowledge" / "commands-registry-v2.json").exists())
|
|
470
|
+
check("hooks_dir", (arkaos_home / "config" / "hooks").exists(), "Run: npx arkaos install")
|
|
471
|
+
|
|
472
|
+
try:
|
|
473
|
+
subprocess.run(["python3", "--version"], capture_output=True, timeout=2)
|
|
474
|
+
check("python", True)
|
|
475
|
+
except Exception:
|
|
476
|
+
check("python", False, "Install Python 3.11+")
|
|
477
|
+
|
|
478
|
+
check("knowledge_db", (arkaos_home / "knowledge.db").exists(), "Run: npx arkaos index")
|
|
479
|
+
|
|
480
|
+
passed = sum(1 for c in checks if c["passed"])
|
|
481
|
+
return {"checks": checks, "passed": passed, "total": len(checks), "healthy": passed == len(checks)}
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
# --- Personas ---
|
|
485
|
+
|
|
486
|
+
def _get_persona_manager():
|
|
487
|
+
try:
|
|
488
|
+
from core.personas.manager import PersonaManager
|
|
489
|
+
db_path = Path.home() / ".arkaos" / "personas.json"
|
|
490
|
+
return PersonaManager(storage_path=db_path)
|
|
491
|
+
except Exception:
|
|
492
|
+
return None
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
@app.get("/api/personas")
|
|
496
|
+
def personas_list():
|
|
497
|
+
mgr = _get_persona_manager()
|
|
498
|
+
if not mgr:
|
|
499
|
+
return {"personas": [], "total": 0}
|
|
500
|
+
personas = mgr.list_all()
|
|
501
|
+
return {"personas": [p.model_dump() for p in personas], "total": len(personas)}
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
@app.get("/api/personas/{persona_id}")
|
|
505
|
+
def persona_detail(persona_id: str):
|
|
506
|
+
mgr = _get_persona_manager()
|
|
507
|
+
if not mgr:
|
|
508
|
+
return {"error": "Persona manager unavailable"}
|
|
509
|
+
p = mgr.get(persona_id)
|
|
510
|
+
if not p:
|
|
511
|
+
return {"error": "Persona not found"}
|
|
512
|
+
return p.model_dump()
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
@app.post("/api/personas")
|
|
516
|
+
def persona_create(body: dict):
|
|
517
|
+
mgr = _get_persona_manager()
|
|
518
|
+
if not mgr:
|
|
519
|
+
return {"error": "Persona manager unavailable"}
|
|
520
|
+
|
|
521
|
+
from core.personas.schema import (
|
|
522
|
+
Persona, PersonaDISC, PersonaEnneagram, PersonaBigFive, PersonaCommunication,
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
# Generate ID from name
|
|
526
|
+
name = body.get("name", "Unknown")
|
|
527
|
+
persona_id = name.lower().replace(" ", "-").replace(".", "")
|
|
528
|
+
|
|
529
|
+
persona = Persona(
|
|
530
|
+
id=persona_id,
|
|
531
|
+
name=name,
|
|
532
|
+
title=body.get("title", ""),
|
|
533
|
+
tagline=body.get("tagline", ""),
|
|
534
|
+
source=body.get("source", name),
|
|
535
|
+
disc=PersonaDISC(**(body.get("disc", {}))),
|
|
536
|
+
enneagram=PersonaEnneagram(**(body.get("enneagram", {}))),
|
|
537
|
+
big_five=PersonaBigFive(**(body.get("big_five", {}))),
|
|
538
|
+
mbti=body.get("mbti", "INTJ"),
|
|
539
|
+
mental_models=body.get("mental_models", []),
|
|
540
|
+
expertise_domains=body.get("expertise_domains", []),
|
|
541
|
+
frameworks=body.get("frameworks", []),
|
|
542
|
+
key_quotes=body.get("key_quotes", []),
|
|
543
|
+
communication=PersonaCommunication(**(body.get("communication", {}))),
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
mgr.create(persona)
|
|
547
|
+
return {"id": persona.id, "created": True}
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
@app.post("/api/personas/{persona_id}/clone")
|
|
551
|
+
def persona_clone(persona_id: str, body: dict = {}):
|
|
552
|
+
mgr = _get_persona_manager()
|
|
553
|
+
if not mgr:
|
|
554
|
+
return {"error": "Persona manager unavailable"}
|
|
555
|
+
|
|
556
|
+
department = body.get("department", "strategy")
|
|
557
|
+
tier = body.get("tier", 2)
|
|
558
|
+
agents_dir = ARKAOS_ROOT / "departments" / department / "agents"
|
|
559
|
+
|
|
560
|
+
agent_id = mgr.clone_to_agent(persona_id, department=department, tier=tier, agents_dir=str(agents_dir))
|
|
561
|
+
if not agent_id:
|
|
562
|
+
return {"error": "Persona not found"}
|
|
563
|
+
|
|
564
|
+
return {"agent_id": agent_id, "department": department, "file": f"departments/{department}/agents/{agent_id}.yaml"}
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
@app.delete("/api/personas/{persona_id}")
|
|
568
|
+
def persona_delete(persona_id: str):
|
|
569
|
+
mgr = _get_persona_manager()
|
|
570
|
+
if not mgr:
|
|
571
|
+
return {"error": "Persona manager unavailable"}
|
|
572
|
+
if mgr.delete(persona_id):
|
|
573
|
+
return {"deleted": True}
|
|
574
|
+
return {"error": "Persona not found"}
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
# --- API Keys ---
|
|
578
|
+
|
|
579
|
+
@app.get("/api/keys")
|
|
580
|
+
def keys_list():
|
|
581
|
+
try:
|
|
582
|
+
from core.keys import list_keys
|
|
583
|
+
return {"keys": list_keys()}
|
|
584
|
+
except Exception:
|
|
585
|
+
return {"keys": []}
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
@app.post("/api/keys")
|
|
589
|
+
def keys_set(body: dict):
|
|
590
|
+
try:
|
|
591
|
+
from core.keys import set_key
|
|
592
|
+
name = body.get("key", "")
|
|
593
|
+
value = body.get("value", "")
|
|
594
|
+
if not name or not value:
|
|
595
|
+
return {"error": "key and value required"}
|
|
596
|
+
set_key(name, value)
|
|
597
|
+
return {"set": True, "key": name}
|
|
598
|
+
except Exception as e:
|
|
599
|
+
return {"error": str(e)}
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
@app.delete("/api/keys/{key_name}")
|
|
603
|
+
def keys_delete(key_name: str):
|
|
604
|
+
try:
|
|
605
|
+
from core.keys import remove_key
|
|
606
|
+
if remove_key(key_name):
|
|
607
|
+
return {"deleted": True}
|
|
608
|
+
return {"error": "Key not found"}
|
|
609
|
+
except Exception as e:
|
|
610
|
+
return {"error": str(e)}
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
@app.get("/api/metrics")
|
|
614
|
+
def metrics():
|
|
615
|
+
metrics_file = Path("/tmp/arkaos-context-cache/hook-metrics.jsonl")
|
|
616
|
+
if not metrics_file.exists():
|
|
617
|
+
return {"entries": [], "avg_ms": 0}
|
|
618
|
+
entries = []
|
|
619
|
+
for line in metrics_file.read_text().strip().split("\n"):
|
|
620
|
+
try:
|
|
621
|
+
entries.append(json.loads(line))
|
|
622
|
+
except Exception:
|
|
623
|
+
continue
|
|
624
|
+
avg_ms = sum(e.get("ms", 0) for e in entries) / len(entries) if entries else 0
|
|
625
|
+
return {"entries": entries[-50:], "avg_ms": round(avg_ms, 1), "total_calls": len(entries)}
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
if __name__ == "__main__":
|
|
629
|
+
import argparse
|
|
630
|
+
parser = argparse.ArgumentParser()
|
|
631
|
+
parser.add_argument("--port", type=int, default=3334)
|
|
632
|
+
parser.add_argument("--host", type=str, default="127.0.0.1")
|
|
633
|
+
args = parser.parse_args()
|
|
634
|
+
|
|
635
|
+
import uvicorn
|
|
636
|
+
uvicorn.run(app, host=args.host, port=args.port, log_level="info")
|