openvox-core 0.1.0__tar.gz
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.
- openvox_core-0.1.0/.gitignore +52 -0
- openvox_core-0.1.0/PKG-INFO +82 -0
- openvox_core-0.1.0/README.md +6 -0
- openvox_core-0.1.0/openvox/__init__.py +3 -0
- openvox_core-0.1.0/openvox/api/__init__.py +3 -0
- openvox_core-0.1.0/openvox/api/app.py +210 -0
- openvox_core-0.1.0/openvox/api/routes/__init__.py +0 -0
- openvox_core-0.1.0/openvox/api/routes/admin.py +133 -0
- openvox_core-0.1.0/openvox/api/routes/agents.py +340 -0
- openvox_core-0.1.0/openvox/api/routes/auth.py +105 -0
- openvox_core-0.1.0/openvox/api/routes/documents.py +133 -0
- openvox_core-0.1.0/openvox/api/routes/evals.py +331 -0
- openvox_core-0.1.0/openvox/api/routes/health.py +19 -0
- openvox_core-0.1.0/openvox/api/routes/jobs.py +260 -0
- openvox_core-0.1.0/openvox/api/routes/mcp.py +81 -0
- openvox_core-0.1.0/openvox/api/routes/playground.py +420 -0
- openvox_core-0.1.0/openvox/api/routes/pricing.py +195 -0
- openvox_core-0.1.0/openvox/api/routes/providers.py +55 -0
- openvox_core-0.1.0/openvox/api/routes/rtc.py +28 -0
- openvox_core-0.1.0/openvox/api/routes/sessions.py +86 -0
- openvox_core-0.1.0/openvox/api/routes/skills.py +31 -0
- openvox_core-0.1.0/openvox/api/routes/storage.py +19 -0
- openvox_core-0.1.0/openvox/api/routes/telephony.py +971 -0
- openvox_core-0.1.0/openvox/api/routes/templates.py +1318 -0
- openvox_core-0.1.0/openvox/api/ws/__init__.py +0 -0
- openvox_core-0.1.0/openvox/api/ws/twilio_stream.py +336 -0
- openvox_core-0.1.0/openvox/api/ws/voice.py +378 -0
- openvox_core-0.1.0/openvox/cli/__init__.py +60 -0
- openvox_core-0.1.0/openvox/cli/__main__.py +12 -0
- openvox_core-0.1.0/openvox/cli/commands/__init__.py +0 -0
- openvox_core-0.1.0/openvox/cli/commands/info.py +94 -0
- openvox_core-0.1.0/openvox/cli/commands/logs.py +89 -0
- openvox_core-0.1.0/openvox/cli/commands/restart.py +18 -0
- openvox_core-0.1.0/openvox/cli/commands/run.py +137 -0
- openvox_core-0.1.0/openvox/cli/commands/start.py +61 -0
- openvox_core-0.1.0/openvox/cli/commands/status.py +22 -0
- openvox_core-0.1.0/openvox/cli/commands/stop.py +20 -0
- openvox_core-0.1.0/openvox/cli/commands/version.py +25 -0
- openvox_core-0.1.0/openvox/cli/daemon/__init__.py +47 -0
- openvox_core-0.1.0/openvox/cli/daemon/base.py +80 -0
- openvox_core-0.1.0/openvox/cli/daemon/launchd.py +174 -0
- openvox_core-0.1.0/openvox/cli/daemon/systemd.py +162 -0
- openvox_core-0.1.0/openvox/cli/daemon/windows_service.py +155 -0
- openvox_core-0.1.0/openvox/cli/main.py +51 -0
- openvox_core-0.1.0/openvox/config.py +174 -0
- openvox_core-0.1.0/openvox/db/__init__.py +5 -0
- openvox_core-0.1.0/openvox/db/models.py +437 -0
- openvox_core-0.1.0/openvox/db/session.py +99 -0
- openvox_core-0.1.0/openvox/eval/__init__.py +15 -0
- openvox_core-0.1.0/openvox/eval/judge.py +136 -0
- openvox_core-0.1.0/openvox/eval/personas.py +84 -0
- openvox_core-0.1.0/openvox/eval/runner.py +208 -0
- openvox_core-0.1.0/openvox/mcp/__init__.py +17 -0
- openvox_core-0.1.0/openvox/mcp/bridge.py +184 -0
- openvox_core-0.1.0/openvox/mcp/catalogue.json +106 -0
- openvox_core-0.1.0/openvox/pipeline/__init__.py +3 -0
- openvox_core-0.1.0/openvox/pipeline/orchestrator.py +649 -0
- openvox_core-0.1.0/openvox/pricing/__init__.py +10 -0
- openvox_core-0.1.0/openvox/pricing/rates.py +345 -0
- openvox_core-0.1.0/openvox/providers/__init__.py +42 -0
- openvox_core-0.1.0/openvox/providers/base.py +238 -0
- openvox_core-0.1.0/openvox/providers/bootstrap.py +53 -0
- openvox_core-0.1.0/openvox/providers/byteplus/__init__.py +8 -0
- openvox_core-0.1.0/openvox/providers/byteplus/llm.py +169 -0
- openvox_core-0.1.0/openvox/providers/byteplus/rtc.py +128 -0
- openvox_core-0.1.0/openvox/providers/byteplus/stt.py +423 -0
- openvox_core-0.1.0/openvox/providers/byteplus/tts.py +185 -0
- openvox_core-0.1.0/openvox/providers/byteplus/voices.py +109 -0
- openvox_core-0.1.0/openvox/providers/openai_compat/__init__.py +30 -0
- openvox_core-0.1.0/openvox/providers/openai_compat/_openai_base.py +121 -0
- openvox_core-0.1.0/openvox/providers/openai_compat/anthropic.py +101 -0
- openvox_core-0.1.0/openvox/providers/openai_compat/assemblyai_stt.py +87 -0
- openvox_core-0.1.0/openvox/providers/openai_compat/cartesia_tts.py +94 -0
- openvox_core-0.1.0/openvox/providers/openai_compat/deepgram_stt.py +94 -0
- openvox_core-0.1.0/openvox/providers/openai_compat/deepseek.py +19 -0
- openvox_core-0.1.0/openvox/providers/openai_compat/elevenlabs_tts.py +71 -0
- openvox_core-0.1.0/openvox/providers/openai_compat/gemini.py +21 -0
- openvox_core-0.1.0/openvox/providers/openai_compat/openai_llm.py +19 -0
- openvox_core-0.1.0/openvox/providers/openai_compat/openai_tts.py +78 -0
- openvox_core-0.1.0/openvox/providers/openai_compat/whisper_stt.py +99 -0
- openvox_core-0.1.0/openvox/providers/registry.py +111 -0
- openvox_core-0.1.0/openvox/providers/vad/__init__.py +14 -0
- openvox_core-0.1.0/openvox/providers/vad/base.py +56 -0
- openvox_core-0.1.0/openvox/providers/vad/silero.py +168 -0
- openvox_core-0.1.0/openvox/rag/__init__.py +22 -0
- openvox_core-0.1.0/openvox/rag/bm25.py +79 -0
- openvox_core-0.1.0/openvox/rag/byteplus_cloud.py +208 -0
- openvox_core-0.1.0/openvox/rag/embeddings.py +56 -0
- openvox_core-0.1.0/openvox/rag/extract.py +135 -0
- openvox_core-0.1.0/openvox/rag/store.py +194 -0
- openvox_core-0.1.0/openvox/scheduler/__init__.py +15 -0
- openvox_core-0.1.0/openvox/scheduler/engine.py +143 -0
- openvox_core-0.1.0/openvox/scheduler/runner.py +298 -0
- openvox_core-0.1.0/openvox/secrets.py +306 -0
- openvox_core-0.1.0/openvox/skills/__init__.py +32 -0
- openvox_core-0.1.0/openvox/skills/base.py +109 -0
- openvox_core-0.1.0/openvox/skills/builtin/__init__.py +20 -0
- openvox_core-0.1.0/openvox/skills/builtin/documents.py +162 -0
- openvox_core-0.1.0/openvox/skills/builtin/ecommerce.py +90 -0
- openvox_core-0.1.0/openvox/skills/builtin/education.py +74 -0
- openvox_core-0.1.0/openvox/skills/builtin/general.py +67 -0
- openvox_core-0.1.0/openvox/skills/builtin/language.py +154 -0
- openvox_core-0.1.0/openvox/skills/builtin/reception.py +299 -0
- openvox_core-0.1.0/openvox/skills/builtin/sales.py +266 -0
- openvox_core-0.1.0/openvox/skills/builtin/setup.py +677 -0
- openvox_core-0.1.0/openvox/skills/builtin/stock.py +91 -0
- openvox_core-0.1.0/openvox/skills/builtin/voice_analysis.py +145 -0
- openvox_core-0.1.0/openvox/skills/registry.py +159 -0
- openvox_core-0.1.0/openvox/skills/runner.py +80 -0
- openvox_core-0.1.0/openvox/skills/watcher.py +107 -0
- openvox_core-0.1.0/openvox/storage/__init__.py +6 -0
- openvox_core-0.1.0/openvox/storage/base.py +24 -0
- openvox_core-0.1.0/openvox/storage/byteplus_tos.py +58 -0
- openvox_core-0.1.0/openvox/storage/factory.py +35 -0
- openvox_core-0.1.0/openvox/storage/local.py +45 -0
- openvox_core-0.1.0/openvox/storage/s3.py +70 -0
- openvox_core-0.1.0/openvox/telephony/__init__.py +9 -0
- openvox_core-0.1.0/openvox/telephony/lark.py +93 -0
- openvox_core-0.1.0/openvox/telephony/telegram.py +212 -0
- openvox_core-0.1.0/openvox/telephony/telegram_polling.py +232 -0
- openvox_core-0.1.0/openvox/telephony/twilio.py +88 -0
- openvox_core-0.1.0/openvox/telephony/wechat_work.py +133 -0
- openvox_core-0.1.0/openvox/telephony/whatsapp_personal.py +213 -0
- openvox_core-0.1.0/openvox/utils/__init__.py +0 -0
- openvox_core-0.1.0/openvox/utils/http.py +89 -0
- openvox_core-0.1.0/openvox/utils/text.py +476 -0
- openvox_core-0.1.0/pyproject.toml +181 -0
- openvox_core-0.1.0/tests/__init__.py +0 -0
- openvox_core-0.1.0/tests/test_daemon_backends.py +194 -0
- openvox_core-0.1.0/tests/test_storage_sqlite_parity.py +200 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Node
|
|
2
|
+
node_modules/
|
|
3
|
+
.pnpm-store/
|
|
4
|
+
*.tsbuildinfo
|
|
5
|
+
.next/
|
|
6
|
+
out/
|
|
7
|
+
dist/
|
|
8
|
+
.turbo/
|
|
9
|
+
|
|
10
|
+
# Python
|
|
11
|
+
__pycache__/
|
|
12
|
+
*.py[cod]
|
|
13
|
+
*$py.class
|
|
14
|
+
*.egg-info/
|
|
15
|
+
.eggs/
|
|
16
|
+
build/
|
|
17
|
+
.pytest_cache/
|
|
18
|
+
.mypy_cache/
|
|
19
|
+
.ruff_cache/
|
|
20
|
+
.venv/
|
|
21
|
+
venv/
|
|
22
|
+
env/
|
|
23
|
+
|
|
24
|
+
# Env
|
|
25
|
+
.env
|
|
26
|
+
.env.local
|
|
27
|
+
.env.*.local
|
|
28
|
+
!.env.example
|
|
29
|
+
|
|
30
|
+
# Editor
|
|
31
|
+
.vscode/
|
|
32
|
+
.idea/
|
|
33
|
+
*.swp
|
|
34
|
+
.DS_Store
|
|
35
|
+
|
|
36
|
+
# Local data (local-first)
|
|
37
|
+
data/
|
|
38
|
+
*.db
|
|
39
|
+
*.sqlite
|
|
40
|
+
*.sqlite3
|
|
41
|
+
|
|
42
|
+
# Logs
|
|
43
|
+
logs/
|
|
44
|
+
*.log
|
|
45
|
+
npm-debug.log*
|
|
46
|
+
pnpm-debug.log*
|
|
47
|
+
|
|
48
|
+
# Docker volumes (when bind-mounted)
|
|
49
|
+
.docker-data/
|
|
50
|
+
|
|
51
|
+
# OpenVox local state directory
|
|
52
|
+
.openvox/
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: openvox-core
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Local-first, open-source voice agent platform — install with pip, run as a daemon, no Docker required.
|
|
5
|
+
Project-URL: Homepage, https://github.com/amznsri/openvox
|
|
6
|
+
Project-URL: Documentation, https://github.com/amznsri/openvox/tree/main/docs
|
|
7
|
+
Project-URL: Repository, https://github.com/amznsri/openvox
|
|
8
|
+
Project-URL: Issues, https://github.com/amznsri/openvox/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/amznsri/openvox/blob/main/docs/SESSION_LOG.md
|
|
10
|
+
Author: OpenVox Contributors
|
|
11
|
+
License: Apache-2.0
|
|
12
|
+
Keywords: agent,ai,rtc,stt,telegram,tts,twilio,voice,whatsapp
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
16
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
17
|
+
Classifier: Operating System :: MacOS :: MacOS X
|
|
18
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
19
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
20
|
+
Classifier: Programming Language :: Python :: 3
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Topic :: Communications :: Telephony
|
|
25
|
+
Classifier: Topic :: Multimedia :: Sound/Audio :: Speech
|
|
26
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
27
|
+
Requires-Python: >=3.11
|
|
28
|
+
Requires-Dist: aiofiles>=24.1.0
|
|
29
|
+
Requires-Dist: aiosqlite>=0.20.0
|
|
30
|
+
Requires-Dist: alembic>=1.13.0
|
|
31
|
+
Requires-Dist: apscheduler>=3.10.4
|
|
32
|
+
Requires-Dist: asyncpg>=0.29.0
|
|
33
|
+
Requires-Dist: authlib>=1.3.0
|
|
34
|
+
Requires-Dist: boto3>=1.35.0
|
|
35
|
+
Requires-Dist: certifi>=2024.8.30
|
|
36
|
+
Requires-Dist: fastapi>=0.115.0
|
|
37
|
+
Requires-Dist: google-cloud-storage>=2.18.0
|
|
38
|
+
Requires-Dist: httpx>=0.27.0
|
|
39
|
+
Requires-Dist: mcp>=1.2.0
|
|
40
|
+
Requires-Dist: numpy>=1.26.0
|
|
41
|
+
Requires-Dist: opentelemetry-api>=1.27.0
|
|
42
|
+
Requires-Dist: opentelemetry-instrumentation-fastapi>=0.48b0
|
|
43
|
+
Requires-Dist: opentelemetry-sdk>=1.27.0
|
|
44
|
+
Requires-Dist: orjson>=3.10.0
|
|
45
|
+
Requires-Dist: oss2>=2.18.0
|
|
46
|
+
Requires-Dist: passlib[bcrypt]>=1.7.4
|
|
47
|
+
Requires-Dist: pydantic-settings>=2.5.0
|
|
48
|
+
Requires-Dist: pydantic>=2.9.0
|
|
49
|
+
Requires-Dist: pydub>=0.25.1
|
|
50
|
+
Requires-Dist: pypdf>=4.3.0
|
|
51
|
+
Requires-Dist: python-jose[cryptography]>=3.3.0
|
|
52
|
+
Requires-Dist: python-multipart>=0.0.12
|
|
53
|
+
Requires-Dist: redis[hiredis]>=5.1.0
|
|
54
|
+
Requires-Dist: scipy>=1.13.0
|
|
55
|
+
Requires-Dist: silero-vad>=5.1.0
|
|
56
|
+
Requires-Dist: sqlalchemy[asyncio]>=2.0.35
|
|
57
|
+
Requires-Dist: structlog>=24.4.0
|
|
58
|
+
Requires-Dist: tenacity>=9.0.0
|
|
59
|
+
Requires-Dist: tos>=2.7.0
|
|
60
|
+
Requires-Dist: twilio>=9.3.0
|
|
61
|
+
Requires-Dist: typer>=0.12.0
|
|
62
|
+
Requires-Dist: uvicorn[standard]>=0.32.0
|
|
63
|
+
Requires-Dist: watchfiles>=0.24.0
|
|
64
|
+
Requires-Dist: websockets>=13.0
|
|
65
|
+
Provides-Extra: dev
|
|
66
|
+
Requires-Dist: mypy>=1.11.0; extra == 'dev'
|
|
67
|
+
Requires-Dist: pytest-asyncio>=0.24.0; extra == 'dev'
|
|
68
|
+
Requires-Dist: pytest-cov>=5.0.0; extra == 'dev'
|
|
69
|
+
Requires-Dist: pytest>=8.3.0; extra == 'dev'
|
|
70
|
+
Requires-Dist: ruff>=0.6.0; extra == 'dev'
|
|
71
|
+
Provides-Extra: local-stt
|
|
72
|
+
Requires-Dist: openai-whisper>=20240930; extra == 'local-stt'
|
|
73
|
+
Provides-Extra: postgres
|
|
74
|
+
Provides-Extra: whatsapp
|
|
75
|
+
Description-Content-Type: text/markdown
|
|
76
|
+
|
|
77
|
+
# openvox-core
|
|
78
|
+
|
|
79
|
+
The OpenVox voice pipeline. FastAPI + asyncio orchestrator that ties STT,
|
|
80
|
+
LLM, TTS, RTC, telephony, and pluggable skills together.
|
|
81
|
+
|
|
82
|
+
See the top-level [README](../../README.md).
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"""FastAPI app factory."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from contextlib import asynccontextmanager
|
|
7
|
+
|
|
8
|
+
from fastapi import FastAPI
|
|
9
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
10
|
+
|
|
11
|
+
from openvox.api.routes import (
|
|
12
|
+
admin as admin_routes,
|
|
13
|
+
agents,
|
|
14
|
+
auth as auth_routes,
|
|
15
|
+
documents as documents_routes,
|
|
16
|
+
evals as evals_routes,
|
|
17
|
+
health,
|
|
18
|
+
jobs as jobs_routes,
|
|
19
|
+
mcp as mcp_routes,
|
|
20
|
+
playground,
|
|
21
|
+
pricing as pricing_routes,
|
|
22
|
+
providers as providers_routes,
|
|
23
|
+
rtc,
|
|
24
|
+
sessions,
|
|
25
|
+
skills as skills_routes,
|
|
26
|
+
storage as storage_routes,
|
|
27
|
+
telephony,
|
|
28
|
+
templates as templates_routes,
|
|
29
|
+
)
|
|
30
|
+
from openvox.api.ws import twilio_stream as twilio_ws
|
|
31
|
+
from openvox.api.ws import voice as voice_ws
|
|
32
|
+
from openvox.config import get_settings
|
|
33
|
+
from openvox.db import init_db
|
|
34
|
+
from openvox.providers.bootstrap import register_builtins
|
|
35
|
+
from openvox.scheduler import start_scheduler, stop_scheduler
|
|
36
|
+
from openvox.skills.registry import get_skill_registry
|
|
37
|
+
from openvox.skills.watcher import start_watcher, stop_watcher
|
|
38
|
+
|
|
39
|
+
logger = logging.getLogger(__name__)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
async def _seed_builtin_personas() -> None:
|
|
43
|
+
"""Upsert the built-in synthetic personas on startup.
|
|
44
|
+
|
|
45
|
+
We upsert by id so edits to the prompt in eval/personas.py take
|
|
46
|
+
effect without manual intervention, but user-edited personas
|
|
47
|
+
(different id) are left alone.
|
|
48
|
+
"""
|
|
49
|
+
from openvox.db import db_session
|
|
50
|
+
from openvox.db.models import Persona
|
|
51
|
+
from openvox.eval.personas import BUILTIN_PERSONAS
|
|
52
|
+
|
|
53
|
+
async with db_session() as s:
|
|
54
|
+
for entry in BUILTIN_PERSONAS:
|
|
55
|
+
existing = await s.get(Persona, entry["id"])
|
|
56
|
+
if existing is None:
|
|
57
|
+
p = Persona(
|
|
58
|
+
id=entry["id"],
|
|
59
|
+
name=entry["name"],
|
|
60
|
+
description=entry.get("description", ""),
|
|
61
|
+
system_prompt=entry["system_prompt"],
|
|
62
|
+
tags=entry.get("tags", []),
|
|
63
|
+
builtin=True,
|
|
64
|
+
)
|
|
65
|
+
s.add(p)
|
|
66
|
+
else:
|
|
67
|
+
# Refresh prompt + tags on every boot so prompt iterations stick.
|
|
68
|
+
existing.name = entry["name"]
|
|
69
|
+
existing.description = entry.get("description", "")
|
|
70
|
+
existing.system_prompt = entry["system_prompt"]
|
|
71
|
+
existing.tags = entry.get("tags", [])
|
|
72
|
+
existing.builtin = True
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@asynccontextmanager
|
|
76
|
+
async def _lifespan(app: FastAPI):
|
|
77
|
+
settings = get_settings()
|
|
78
|
+
settings.data_dir.mkdir(parents=True, exist_ok=True)
|
|
79
|
+
register_builtins()
|
|
80
|
+
get_skill_registry() # discover
|
|
81
|
+
await init_db()
|
|
82
|
+
await _seed_builtin_personas()
|
|
83
|
+
await start_scheduler()
|
|
84
|
+
await start_watcher() # hot-reload skills dropped in ~/.openvox/skills/
|
|
85
|
+
# Phase 2: start telegram polling tasks for any agent connected
|
|
86
|
+
# in polling mode. Webhook-mode agents bootstrap themselves on the
|
|
87
|
+
# next incoming Telegram POST — no startup action needed.
|
|
88
|
+
from openvox.telephony.telegram_polling import start_all_pollers, stop_all_pollers
|
|
89
|
+
await start_all_pollers()
|
|
90
|
+
# WhatsApp Personal: reconnect bridge sessions for any agent whose
|
|
91
|
+
# channels.whatsapp_personal.enabled == true. No-op if the bridge
|
|
92
|
+
# container isn't running (opt-in via --profile whatsapp).
|
|
93
|
+
from openvox.telephony.whatsapp_personal import (
|
|
94
|
+
start_all_sessions as wpp_start_all,
|
|
95
|
+
stop_all_sessions as wpp_stop_all,
|
|
96
|
+
)
|
|
97
|
+
await wpp_start_all()
|
|
98
|
+
logger.info("OpenVox core started — auth=%s storage=%s", settings.openvox_auth, settings.storage_backend)
|
|
99
|
+
yield
|
|
100
|
+
await wpp_stop_all()
|
|
101
|
+
await stop_all_pollers()
|
|
102
|
+
await stop_watcher()
|
|
103
|
+
await stop_scheduler()
|
|
104
|
+
logger.info("OpenVox core shutting down")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def create_app() -> FastAPI:
|
|
108
|
+
settings = get_settings()
|
|
109
|
+
logging.basicConfig(level=settings.log_level.upper())
|
|
110
|
+
|
|
111
|
+
app = FastAPI(
|
|
112
|
+
title="OpenVox Core",
|
|
113
|
+
description="Voice agent pipeline (STT + LLM + TTS + RTC + telephony).",
|
|
114
|
+
version="0.1.0",
|
|
115
|
+
lifespan=_lifespan,
|
|
116
|
+
)
|
|
117
|
+
app.add_middleware(
|
|
118
|
+
CORSMiddleware,
|
|
119
|
+
allow_origins=["*"], # local-first — accept dashboard from any local origin
|
|
120
|
+
allow_credentials=True,
|
|
121
|
+
allow_methods=["*"],
|
|
122
|
+
allow_headers=["*"],
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
app.include_router(health.router)
|
|
126
|
+
app.include_router(auth_routes.router, prefix="/api/v1/auth", tags=["auth"])
|
|
127
|
+
app.include_router(admin_routes.router, prefix="/api/v1/admin", tags=["admin"])
|
|
128
|
+
app.include_router(agents.router, prefix="/api/v1/agents", tags=["agents"])
|
|
129
|
+
app.include_router(documents_routes.router, prefix="/api/v1/agents", tags=["documents"])
|
|
130
|
+
app.include_router(sessions.router, prefix="/api/v1/sessions", tags=["sessions"])
|
|
131
|
+
app.include_router(providers_routes.router, prefix="/api/v1/providers", tags=["providers"])
|
|
132
|
+
app.include_router(skills_routes.router, prefix="/api/v1/skills", tags=["skills"])
|
|
133
|
+
app.include_router(templates_routes.router, prefix="/api/v1/templates", tags=["templates"])
|
|
134
|
+
app.include_router(rtc.router, prefix="/api/v1/rtc", tags=["rtc"])
|
|
135
|
+
app.include_router(jobs_routes.router, prefix="/api/v1/jobs", tags=["jobs"])
|
|
136
|
+
app.include_router(mcp_routes.router, prefix="/api/v1/mcp", tags=["mcp"])
|
|
137
|
+
app.include_router(telephony.router, prefix="/api/v1/telephony", tags=["telephony"])
|
|
138
|
+
app.include_router(pricing_routes.router, prefix="/api/v1/pricing", tags=["pricing"])
|
|
139
|
+
app.include_router(evals_routes.router, prefix="/api/v1/evals", tags=["evals"])
|
|
140
|
+
app.include_router(playground.router, prefix="/api/v1/playground", tags=["playground"])
|
|
141
|
+
app.include_router(storage_routes.router, prefix="/storage", tags=["storage"])
|
|
142
|
+
app.include_router(voice_ws.router)
|
|
143
|
+
app.include_router(twilio_ws.router)
|
|
144
|
+
|
|
145
|
+
# ── Optional static-dashboard serving ───────────────────────────
|
|
146
|
+
# Phase 1 PR-3 scaffolding: when the dashboard has been built with
|
|
147
|
+
# BUILD_OUTPUT=export (Next.js static export → `out/` directory),
|
|
148
|
+
# FastAPI serves it at /dashboard/* on the same port as the API.
|
|
149
|
+
# This is what enables the single-process CLI experience
|
|
150
|
+
# (`openvox run` → one process, browser opens to localhost:8000/dashboard).
|
|
151
|
+
#
|
|
152
|
+
# Discovery order — first existing path wins:
|
|
153
|
+
# 1. OPENVOX_DASHBOARD_PATH env var (explicit override)
|
|
154
|
+
# 2. /app/dashboard_static/ (Docker/CLI install — bundled at build)
|
|
155
|
+
# 3. ../apps/dashboard/out/ (repo-relative dev workflow)
|
|
156
|
+
#
|
|
157
|
+
# If none of the above exist, the mount is silently skipped —
|
|
158
|
+
# browsers hitting /dashboard get 404, which is correct for Docker
|
|
159
|
+
# mode (where the separate `openvox-dashboard` container handles it).
|
|
160
|
+
#
|
|
161
|
+
# Today this is no-op: the dashboard's static-export build pipeline
|
|
162
|
+
# ships in a follow-up commit (the agents/[id] route needs to be
|
|
163
|
+
# refactored to query params first — Next.js static export can't
|
|
164
|
+
# handle dynamic path params for runtime-created IDs). The mount
|
|
165
|
+
# itself is committed now so we don't have to revisit api/app.py
|
|
166
|
+
# in that follow-up — just produce the out/ directory and it
|
|
167
|
+
# gets served automatically.
|
|
168
|
+
_maybe_mount_dashboard_static(app)
|
|
169
|
+
|
|
170
|
+
return app
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _maybe_mount_dashboard_static(app: FastAPI) -> None:
|
|
174
|
+
"""Mount the built dashboard static files under /dashboard/* if available.
|
|
175
|
+
|
|
176
|
+
See the comment block in create_app() above for the discovery
|
|
177
|
+
rules + why this is a no-op today. Pulled into its own function so
|
|
178
|
+
it can be unit-tested + so the mount logic doesn't clutter the
|
|
179
|
+
main app factory.
|
|
180
|
+
"""
|
|
181
|
+
import os
|
|
182
|
+
from pathlib import Path
|
|
183
|
+
|
|
184
|
+
from fastapi.staticfiles import StaticFiles
|
|
185
|
+
|
|
186
|
+
explicit = os.environ.get("OPENVOX_DASHBOARD_PATH")
|
|
187
|
+
candidates: list[Path] = []
|
|
188
|
+
if explicit:
|
|
189
|
+
candidates.append(Path(explicit))
|
|
190
|
+
candidates.append(Path("/app/dashboard_static"))
|
|
191
|
+
# Repo-relative — works in `openvox run` invoked from the repo root.
|
|
192
|
+
candidates.append(Path(__file__).resolve().parent.parent.parent.parent.parent
|
|
193
|
+
/ "apps" / "dashboard" / "out")
|
|
194
|
+
|
|
195
|
+
for path in candidates:
|
|
196
|
+
if path.is_dir() and (path / "index.html").exists():
|
|
197
|
+
app.mount(
|
|
198
|
+
"/dashboard",
|
|
199
|
+
StaticFiles(directory=str(path), html=True),
|
|
200
|
+
name="dashboard",
|
|
201
|
+
)
|
|
202
|
+
logger.info("dashboard static files mounted at /dashboard from %s", path)
|
|
203
|
+
return
|
|
204
|
+
|
|
205
|
+
# Not found — that's fine in Docker mode where the separate
|
|
206
|
+
# `openvox-dashboard` container serves the dashboard on its own port.
|
|
207
|
+
logger.debug(
|
|
208
|
+
"no dashboard static files found — /dashboard will 404. "
|
|
209
|
+
"Either set OPENVOX_DASHBOARD_PATH or use Docker dashboard service."
|
|
210
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Admin endpoints — backing API for the dashboard's first-run wizard.
|
|
2
|
+
|
|
3
|
+
Phase 3 of `docs/PLANNING_SESSION15.md`. Three endpoints:
|
|
4
|
+
|
|
5
|
+
- ``GET /api/v1/admin/setup/status`` — what's configured + is setup complete?
|
|
6
|
+
- ``POST /api/v1/admin/setup/keys`` — save provider keys (encrypted-at-rest)
|
|
7
|
+
- ``DELETE /api/v1/admin/setup/keys`` — remove a stored key
|
|
8
|
+
|
|
9
|
+
The dashboard's wizard pages (``apps/dashboard/src/app/dashboard/setup/*``)
|
|
10
|
+
poll status on mount, present the form when ``complete == false``, and
|
|
11
|
+
POST the user's pasted keys here. The encrypted-store layer
|
|
12
|
+
(``openvox/secrets.py``) handles persistence; this module is just the
|
|
13
|
+
HTTP shell.
|
|
14
|
+
|
|
15
|
+
Security notes
|
|
16
|
+
==============
|
|
17
|
+
- Endpoints don't require auth today because OpenVox runs local-first;
|
|
18
|
+
the operator and the admin are the same person. When
|
|
19
|
+
``OPENVOX_AUTH=enabled`` (multi-tenant cloud), these must be gated
|
|
20
|
+
behind admin-only authorization — see the TODO at the top of
|
|
21
|
+
``check_admin()`` below.
|
|
22
|
+
- POSTed values flow into Fernet-encrypted storage. Never logged or
|
|
23
|
+
returned in any response.
|
|
24
|
+
"""
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import logging
|
|
28
|
+
from typing import Any
|
|
29
|
+
|
|
30
|
+
from fastapi import APIRouter, HTTPException
|
|
31
|
+
from pydantic import BaseModel, Field
|
|
32
|
+
|
|
33
|
+
from openvox import secrets as secret_store
|
|
34
|
+
|
|
35
|
+
logger = logging.getLogger(__name__)
|
|
36
|
+
|
|
37
|
+
router = APIRouter()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def check_admin() -> None:
|
|
41
|
+
"""Authorization placeholder for the multi-tenant future.
|
|
42
|
+
|
|
43
|
+
Today: no-op. OpenVox runs local-first; operator == admin.
|
|
44
|
+
|
|
45
|
+
When ``OPENVOX_AUTH=enabled`` ships (cloud mode), this should
|
|
46
|
+
inspect the request's bearer token and 403 if the caller isn't
|
|
47
|
+
an admin. Keep all admin endpoints calling this so the eventual
|
|
48
|
+
gate has a single edit point.
|
|
49
|
+
"""
|
|
50
|
+
# TODO: when OPENVOX_AUTH lands, verify admin role from JWT.
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class SetupKeysRequest(BaseModel):
|
|
55
|
+
"""Save one or more provider keys at once.
|
|
56
|
+
|
|
57
|
+
Example body:
|
|
58
|
+
{
|
|
59
|
+
"provider": "byteplus",
|
|
60
|
+
"keys": {
|
|
61
|
+
"llm_api_key": "01a2b3c4...",
|
|
62
|
+
"voice_api_key": "01a2b3c4..."
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
An empty string for a key value deletes the stored key (caller
|
|
67
|
+
can use this to "forget" a key without a separate DELETE call).
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
provider: str = Field(..., min_length=1, max_length=64)
|
|
71
|
+
keys: dict[str, str] = Field(default_factory=dict)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@router.get("/setup/status")
|
|
75
|
+
async def setup_status() -> dict[str, Any]:
|
|
76
|
+
"""Tell the dashboard whether the first-run wizard should run.
|
|
77
|
+
|
|
78
|
+
Used by ``apps/dashboard/src/app/dashboard/layout.tsx`` (or
|
|
79
|
+
equivalent) on mount: if ``complete == false`` and the user
|
|
80
|
+
isn't already on /dashboard/setup, redirect them there.
|
|
81
|
+
"""
|
|
82
|
+
check_admin()
|
|
83
|
+
return await secret_store.setup_complete()
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@router.post("/setup/keys")
|
|
87
|
+
async def setup_keys(req: SetupKeysRequest) -> dict[str, Any]:
|
|
88
|
+
"""Persist a batch of provider keys.
|
|
89
|
+
|
|
90
|
+
Returns the updated setup-status so the wizard can refresh its
|
|
91
|
+
"what's configured" panel without a second round-trip.
|
|
92
|
+
"""
|
|
93
|
+
check_admin()
|
|
94
|
+
|
|
95
|
+
provider = req.provider.strip().lower()
|
|
96
|
+
if not provider:
|
|
97
|
+
raise HTTPException(400, "provider is required")
|
|
98
|
+
if not req.keys:
|
|
99
|
+
raise HTTPException(400, "keys map must be non-empty")
|
|
100
|
+
|
|
101
|
+
saved: list[str] = []
|
|
102
|
+
deleted: list[str] = []
|
|
103
|
+
for key_name, value in req.keys.items():
|
|
104
|
+
try:
|
|
105
|
+
await secret_store.set_provider_key(provider, key_name, value)
|
|
106
|
+
except ValueError as e:
|
|
107
|
+
raise HTTPException(400, f"invalid key '{key_name}': {e}") from e
|
|
108
|
+
(deleted if value == "" else saved).append(key_name)
|
|
109
|
+
|
|
110
|
+
logger.info(
|
|
111
|
+
"admin/setup/keys: provider=%s saved=%s deleted=%s",
|
|
112
|
+
provider, saved, deleted,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
"ok": True,
|
|
117
|
+
"saved": saved,
|
|
118
|
+
"deleted": deleted,
|
|
119
|
+
"status": await secret_store.setup_complete(),
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class DeleteKeyRequest(BaseModel):
|
|
124
|
+
provider: str = Field(..., min_length=1, max_length=64)
|
|
125
|
+
key_name: str = Field(..., min_length=1, max_length=64)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@router.delete("/setup/keys")
|
|
129
|
+
async def delete_key(req: DeleteKeyRequest) -> dict[str, Any]:
|
|
130
|
+
"""Remove a single stored key — env-var fallback resumes."""
|
|
131
|
+
check_admin()
|
|
132
|
+
await secret_store.delete_provider_key(req.provider, req.key_name)
|
|
133
|
+
return {"ok": True, "status": await secret_store.setup_complete()}
|