openring 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.
openring/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """OpenRing: open-source, self-hosted voice AI agents with a Vapi-compatible API."""
2
+
3
+ __version__ = "0.1.0"
openring/__main__.py ADDED
@@ -0,0 +1,118 @@
1
+ """`openring` command: first run creates ~/.openring with a generated API key, then starts the server."""
2
+
3
+ import argparse
4
+ import os
5
+ import secrets
6
+ import socket
7
+ from pathlib import Path
8
+
9
+ HOME = Path(os.environ.get("OPENRING_HOME", Path.home() / ".openring"))
10
+ ENV_FILE = HOME / ".env"
11
+
12
+ ENV_TEMPLATE = """# OpenRing settings. Edit and restart `openring` to apply.
13
+ OPENRING_API_KEY={key}
14
+ # Dashboard password. Leave empty to choose it in the browser on first visit (from this computer).
15
+ # Set it for servers you reach over the network (Docker, VPS). Forgot it? Run: openring --reset-password
16
+ OPENRING_PASSWORD=
17
+ # Public https URL Twilio can reach (e.g. from `ngrok http 8000`)
18
+ PUBLIC_URL=
19
+ TWILIO_ACCOUNT_SID=
20
+ # Required to answer calls: it proves each webhook really comes from Twilio
21
+ TWILIO_AUTH_TOKEN=
22
+ # Speech-to-text: Deepgram gives US$200 free credit to new accounts
23
+ DEEPGRAM_API_KEY=
24
+ # LLM: any of these (or run OmniRoute locally for free models)
25
+ OPENAI_API_KEY=
26
+ GROQ_API_KEY=
27
+ OPENROUTER_API_KEY=
28
+ OMNIROUTE_BASE_URL=http://localhost:20128/v1
29
+ # Text-to-speech: leave empty to use Kokoro (free, local)
30
+ ELEVENLABS_API_KEY=
31
+ """
32
+
33
+
34
+ def load_env_file(path: Path) -> None:
35
+ """Load KEY=VALUE lines; real environment variables win."""
36
+ if not path.exists():
37
+ return
38
+ for line in path.read_text(encoding="utf-8").splitlines():
39
+ line = line.strip()
40
+ if not line or line.startswith("#") or "=" not in line:
41
+ continue
42
+ key, value = line.split("=", 1)
43
+ if value.strip():
44
+ os.environ.setdefault(key.strip(), value.strip().strip('"').strip("'"))
45
+
46
+
47
+ def first_run_setup() -> bool:
48
+ if ENV_FILE.exists():
49
+ return False
50
+ HOME.mkdir(parents=True, exist_ok=True)
51
+ ENV_FILE.write_text(ENV_TEMPLATE.format(key=secrets.token_urlsafe(24)), encoding="utf-8")
52
+ return True
53
+
54
+
55
+ def port_in_use(host: str, port: int) -> bool:
56
+ with socket.socket(socket.AF_INET6 if ":" in host else socket.AF_INET) as s:
57
+ if os.name != "nt": # like uvicorn, ignore connections still closing from a previous run
58
+ s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
59
+ try:
60
+ s.bind((host, port))
61
+ except OSError:
62
+ return True
63
+ return False
64
+
65
+
66
+ def ensure_port_free(host: str, port: int) -> None:
67
+ if port_in_use(host, port):
68
+ print(f"\n Port {port} is already in use, maybe by another OpenRing.")
69
+ print(f" Stop that program, or start on another port: openring --port {port + 1}\n")
70
+ raise SystemExit(1)
71
+
72
+
73
+ def reset_password(db_path: str) -> None:
74
+ from .auth import Sessions
75
+ from .store import Store
76
+
77
+ Sessions(Store(db_path)).reset_password()
78
+ print("\n Dashboard password removed. Start openring and open http://localhost to choose a new one.")
79
+ if os.environ.get("OPENRING_PASSWORD"):
80
+ print(" OPENRING_PASSWORD is set, so it becomes the password again on the next start.")
81
+ print()
82
+
83
+
84
+ def main(argv: list[str] | None = None) -> None:
85
+ parser = argparse.ArgumentParser(prog="openring", description="Self-hosted voice AI agents, Vapi-compatible.")
86
+ parser.add_argument("--port", type=int, help="port to serve on (default: $PORT, or 8000)")
87
+ parser.add_argument("--reset-password", action="store_true",
88
+ help="forget the dashboard password; choose a new one at http://localhost on the next visit")
89
+ args = parser.parse_args(argv)
90
+
91
+ created = first_run_setup()
92
+ load_env_file(Path.cwd() / ".env") # a project-local .env takes precedence
93
+ load_env_file(ENV_FILE)
94
+ os.environ.setdefault("OPENRING_DB_PATH", str(HOME / "openring.db"))
95
+ if args.reset_password:
96
+ reset_password(os.environ["OPENRING_DB_PATH"])
97
+ return
98
+ host = os.environ.get("HOST", "127.0.0.1")
99
+ port = args.port or int(os.environ.get("PORT", "8000"))
100
+ ensure_port_free(host, port)
101
+
102
+ import uvicorn
103
+
104
+ from .api import create_app
105
+ from .providers import kokoro_installed, kokoro_ready, warm_up
106
+
107
+ os.environ["OPENRING_ENV_FILE"] = str(ENV_FILE) # error messages point people to it
108
+ print(f"\n OpenRing is running: http://localhost:{port}")
109
+ print(f" Settings: {ENV_FILE}" + (" (created now)" if created else ""))
110
+ if kokoro_installed() and not kokoro_ready():
111
+ print(" Downloading the free Kokoro voice in the background (one time, about 120 MB).")
112
+ warm_up()
113
+ print(" Press Ctrl+C to stop.\n")
114
+ uvicorn.run(create_app(), host=host, port=port, log_level="warning")
115
+
116
+
117
+ if __name__ == "__main__":
118
+ main()