pythia-knowledge 1.0.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.
- pythia/__init__.py +3 -0
- pythia/cli.py +246 -0
- pythia/connectors/__init__.py +0 -0
- pythia/connectors/claude_code.py +467 -0
- pythia/connectors/common.py +99 -0
- pythia/connectors/confluence.py +803 -0
- pythia/connectors/github.py +444 -0
- pythia/connectors/incident_io.py +365 -0
- pythia/connectors/slack.py +674 -0
- pythia/consumer.py +268 -0
- pythia/daemon.py +650 -0
- pythia/db.py +80 -0
- pythia/docker-compose.yml +15 -0
- pythia/embed.py +39 -0
- pythia/events.py +339 -0
- pythia/evolution.py +392 -0
- pythia/ids.py +12 -0
- pythia/jobs.py +103 -0
- pythia/knowledge.py +494 -0
- pythia/llm/__init__.py +0 -0
- pythia/llm/client.py +328 -0
- pythia/mcp_server.py +73 -0
- pythia/migrations/001_schema.sql +536 -0
- pythia/migrations/__init__.py +0 -0
- pythia/pipeline/__init__.py +0 -0
- pythia/pipeline/adjudicate.py +297 -0
- pythia/pipeline/batch.py +254 -0
- pythia/pipeline/correct.py +227 -0
- pythia/pipeline/derive.py +317 -0
- pythia/pipeline/live.py +149 -0
- pythia/pipeline/process.py +472 -0
- pythia/pipeline/prompts.py +260 -0
- pythia/pipeline/reconcile.py +168 -0
- pythia/pipeline/schemas.py +154 -0
- pythia/pipeline/sweep.py +155 -0
- pythia/quality.py +359 -0
- pythia/reexplain.py +248 -0
- pythia/retrieval.py +137 -0
- pythia/services.py +41 -0
- pythia/settings.py +194 -0
- pythia/usage.py +135 -0
- pythia/value.py +265 -0
- pythia/vault.py +31 -0
- pythia/web/__init__.py +0 -0
- pythia/web/app.py +779 -0
- pythia/web/graph.py +167 -0
- pythia/web/processing.py +131 -0
- pythia/web/static/explore.js +242 -0
- pythia/web/static/style.css +120 -0
- pythia/web/templates/_now.html +25 -0
- pythia/web/templates/base.html +30 -0
- pythia/web/templates/config.html +231 -0
- pythia/web/templates/costs.html +30 -0
- pythia/web/templates/evolution.html +153 -0
- pythia/web/templates/explore.html +36 -0
- pythia/web/templates/label.html +22 -0
- pythia/web/templates/quality.html +141 -0
- pythia/web/templates/sources.html +249 -0
- pythia/web/templates/usage.html +33 -0
- pythia_knowledge-1.0.0.dist-info/METADATA +197 -0
- pythia_knowledge-1.0.0.dist-info/RECORD +64 -0
- pythia_knowledge-1.0.0.dist-info/WHEEL +4 -0
- pythia_knowledge-1.0.0.dist-info/entry_points.txt +2 -0
- pythia_knowledge-1.0.0.dist-info/licenses/LICENSE +21 -0
pythia/__init__.py
ADDED
pythia/cli.py
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"""Minimal CLI: starts the background process (daemon + web) and nothing else.
|
|
2
|
+
|
|
3
|
+
pythia start starts the database (Docker) and the background process
|
|
4
|
+
pythia stop stops the process
|
|
5
|
+
pythia status shows whether it is running and where
|
|
6
|
+
pythia run runs in the foreground (what `start` executes)
|
|
7
|
+
pythia autostart starts by itself at macOS login (LaunchAgent); `--off` undoes it
|
|
8
|
+
pythia boot what the LaunchAgent executes: waits for Docker Desktop and runs `start`
|
|
9
|
+
|
|
10
|
+
All configuration happens on the web.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import argparse
|
|
16
|
+
import logging
|
|
17
|
+
import os
|
|
18
|
+
import signal
|
|
19
|
+
import subprocess
|
|
20
|
+
import sys
|
|
21
|
+
import time
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
from . import __version__
|
|
25
|
+
|
|
26
|
+
HOME = Path(os.environ.get("PYTHIA_HOME", Path.home() / ".pythia"))
|
|
27
|
+
PIDFILE = HOME / "pythia.pid"
|
|
28
|
+
LOGFILE = HOME / "pythia.log"
|
|
29
|
+
REPO = Path(__file__).resolve().parents[2]
|
|
30
|
+
CONTAINER = "pythia-db"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _compose_file() -> Path:
|
|
34
|
+
"""docker-compose.yml shipped inside the package (pip install) or at the repository root (source checkout)."""
|
|
35
|
+
for candidate in (Path(__file__).parent / "docker-compose.yml", REPO / "docker-compose.yml"):
|
|
36
|
+
if candidate.exists():
|
|
37
|
+
return candidate
|
|
38
|
+
raise SystemExit("docker-compose.yml not found; set PYTHIA_DATABASE_URL to use your own Postgres")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _pid() -> int | None:
|
|
42
|
+
try:
|
|
43
|
+
pid = int(PIDFILE.read_text().strip())
|
|
44
|
+
os.kill(pid, 0)
|
|
45
|
+
return pid
|
|
46
|
+
except (FileNotFoundError, ValueError, ProcessLookupError, PermissionError):
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def ensure_database() -> None:
|
|
51
|
+
if os.environ.get("PYTHIA_DATABASE_URL"):
|
|
52
|
+
return
|
|
53
|
+
state = subprocess.run(["docker", "inspect", "-f", "{{.State.Running}}", CONTAINER],
|
|
54
|
+
capture_output=True, text=True)
|
|
55
|
+
if state.returncode == 0:
|
|
56
|
+
if state.stdout.strip() != "true":
|
|
57
|
+
subprocess.run(["docker", "start", CONTAINER], check=True, capture_output=True)
|
|
58
|
+
else:
|
|
59
|
+
subprocess.run(["docker", "compose", "-p", "pythia", "-f", str(_compose_file()), "up", "-d"], check=True)
|
|
60
|
+
from .db import database_url
|
|
61
|
+
import psycopg
|
|
62
|
+
for _ in range(30):
|
|
63
|
+
try:
|
|
64
|
+
psycopg.connect(database_url(), connect_timeout=2).close()
|
|
65
|
+
return
|
|
66
|
+
except psycopg.OperationalError:
|
|
67
|
+
time.sleep(1)
|
|
68
|
+
raise SystemExit("Postgres did not respond in 30s")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def cmd_run(_args) -> None:
|
|
72
|
+
import uvicorn
|
|
73
|
+
|
|
74
|
+
from .daemon import Daemon
|
|
75
|
+
from .db import Database
|
|
76
|
+
from .services import build
|
|
77
|
+
from .web.app import create_app
|
|
78
|
+
|
|
79
|
+
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s")
|
|
80
|
+
ensure_database()
|
|
81
|
+
db = Database()
|
|
82
|
+
db.open()
|
|
83
|
+
svc = build(db)
|
|
84
|
+
daemon = Daemon(svc)
|
|
85
|
+
daemon.start()
|
|
86
|
+
port = int(svc.settings.get("web")["port"])
|
|
87
|
+
try:
|
|
88
|
+
uvicorn.run(create_app(svc), host="127.0.0.1", port=port, log_level="warning")
|
|
89
|
+
finally:
|
|
90
|
+
daemon.shutdown()
|
|
91
|
+
db.close()
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def cmd_start(_args) -> None:
|
|
95
|
+
running = _all_pids()
|
|
96
|
+
if running:
|
|
97
|
+
print(f"Already running (pid {', '.join(map(str, running))}). Use `pythia stop` first.")
|
|
98
|
+
return
|
|
99
|
+
HOME.mkdir(parents=True, exist_ok=True)
|
|
100
|
+
ensure_database()
|
|
101
|
+
log = open(LOGFILE, "ab")
|
|
102
|
+
proc = subprocess.Popen([sys.executable, "-m", "pythia.cli", "run"], stdout=log, stderr=log,
|
|
103
|
+
stdin=subprocess.DEVNULL, start_new_session=True)
|
|
104
|
+
PIDFILE.write_text(str(proc.pid))
|
|
105
|
+
time.sleep(2)
|
|
106
|
+
if proc.poll() is not None:
|
|
107
|
+
raise SystemExit(f"Failed to start; see {LOGFILE}")
|
|
108
|
+
print(f"Pythia running (pid {proc.pid}) — http://127.0.0.1:{_port()} · log: {LOGFILE}")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _port() -> int:
|
|
112
|
+
try:
|
|
113
|
+
from .db import Database
|
|
114
|
+
from .settings import Settings
|
|
115
|
+
db = Database(min_size=1, max_size=1)
|
|
116
|
+
db.open()
|
|
117
|
+
port = int(Settings(db).get("web")["port"])
|
|
118
|
+
db.close()
|
|
119
|
+
return port
|
|
120
|
+
except Exception:
|
|
121
|
+
return 7117
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _all_pids() -> list[int]:
|
|
125
|
+
"""All `pythia run` processes (not just the pidfile's): an orphan daemon running old code
|
|
126
|
+
would pick up jobs from the queue and fail on them."""
|
|
127
|
+
out = subprocess.run(["pgrep", "-f", "pythia.cli run"], capture_output=True, text=True)
|
|
128
|
+
pids = [int(p) for p in out.stdout.split() if p.strip() and int(p) != os.getpid()]
|
|
129
|
+
# Python processes only: a shell whose command line mentions the text is not the daemon.
|
|
130
|
+
return [p for p in pids if "python" in subprocess.run(["ps", "-o", "comm=", "-p", str(p)],
|
|
131
|
+
capture_output=True, text=True).stdout.lower()]
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def cmd_stop(_args) -> None:
|
|
135
|
+
pids = sorted(set(_all_pids()) | ({_pid()} if _pid() else set()))
|
|
136
|
+
if not pids:
|
|
137
|
+
print("Not running.")
|
|
138
|
+
return
|
|
139
|
+
for pid in pids:
|
|
140
|
+
try:
|
|
141
|
+
os.kill(pid, signal.SIGTERM)
|
|
142
|
+
except ProcessLookupError:
|
|
143
|
+
pass
|
|
144
|
+
for _ in range(20):
|
|
145
|
+
if not _all_pids():
|
|
146
|
+
break
|
|
147
|
+
time.sleep(0.5)
|
|
148
|
+
for pid in _all_pids(): # did not exit in 10s: force it
|
|
149
|
+
try:
|
|
150
|
+
os.kill(pid, signal.SIGKILL)
|
|
151
|
+
except ProcessLookupError:
|
|
152
|
+
pass
|
|
153
|
+
PIDFILE.unlink(missing_ok=True)
|
|
154
|
+
print("Stopped." if len(pids) == 1 else f"Stopped ({len(pids)} processes).")
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def cmd_status(_args) -> None:
|
|
158
|
+
pid = _pid()
|
|
159
|
+
print(f"Running (pid {pid}) — http://127.0.0.1:{_port()}" if pid else "Stopped.")
|
|
160
|
+
print("Starts by itself at login (LaunchAgent)." if AGENT_PLIST.exists() else "Does not start by itself at login (`pythia autostart`).")
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
# ---------------------------------------------------------------- start at login (macOS)
|
|
164
|
+
|
|
165
|
+
AGENT_LABEL = "local.pythia"
|
|
166
|
+
AGENT_PLIST = Path.home() / "Library" / "LaunchAgents" / f"{AGENT_LABEL}.plist"
|
|
167
|
+
AUTOSTART_LOG = HOME / "autostart.log"
|
|
168
|
+
DOCKER_PATHS = ["/usr/local/bin", "/opt/homebrew/bin", "/Applications/Docker.app/Contents/Resources/bin", "/usr/bin", "/bin"]
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _docker_ready() -> bool:
|
|
172
|
+
try:
|
|
173
|
+
return subprocess.run(["docker", "info"], capture_output=True, timeout=20).returncode == 0
|
|
174
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
175
|
+
return False
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def cmd_boot(_args) -> None:
|
|
179
|
+
"""At login: Postgres runs in Docker Desktop, so it must be up first. Opens
|
|
180
|
+
Docker Desktop in the background if it is not open, waits up to 5 min and runs `start`."""
|
|
181
|
+
def stamp() -> str:
|
|
182
|
+
return time.strftime("%Y-%m-%d %H:%M:%S")
|
|
183
|
+
|
|
184
|
+
if os.environ.get("PYTHIA_DATABASE_URL") is None and not _docker_ready():
|
|
185
|
+
print(f"{stamp()} Docker Desktop is not responding; opening it in the background.", flush=True)
|
|
186
|
+
subprocess.run(["open", "-g", "-a", "Docker"], capture_output=True)
|
|
187
|
+
for _ in range(60):
|
|
188
|
+
time.sleep(5)
|
|
189
|
+
if _docker_ready():
|
|
190
|
+
break
|
|
191
|
+
else:
|
|
192
|
+
raise SystemExit(f"{stamp()} Docker Desktop did not respond in 5 min; Pythia did not start. "
|
|
193
|
+
"Open Docker Desktop and run `pythia start`.")
|
|
194
|
+
print(f"{stamp()} Docker ready; starting Pythia.", flush=True)
|
|
195
|
+
cmd_start(_args)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def cmd_autostart(args) -> None:
|
|
199
|
+
import plistlib
|
|
200
|
+
|
|
201
|
+
uid = os.getuid()
|
|
202
|
+
if args.off:
|
|
203
|
+
if AGENT_PLIST.exists():
|
|
204
|
+
subprocess.run(["launchctl", "bootout", f"gui/{uid}", str(AGENT_PLIST)], capture_output=True)
|
|
205
|
+
AGENT_PLIST.unlink()
|
|
206
|
+
print("Pythia no longer starts by itself at login.")
|
|
207
|
+
return
|
|
208
|
+
HOME.mkdir(parents=True, exist_ok=True)
|
|
209
|
+
AGENT_PLIST.parent.mkdir(parents=True, exist_ok=True)
|
|
210
|
+
plist = {
|
|
211
|
+
"Label": AGENT_LABEL,
|
|
212
|
+
"ProgramArguments": [sys.executable, "-m", "pythia.cli", "boot"],
|
|
213
|
+
"RunAtLoad": True,
|
|
214
|
+
# `start` leaves the process running on its own; the agent ends right afterwards. Without
|
|
215
|
+
# KeepAlive, so that `pythia stop` keeps really stopping it.
|
|
216
|
+
"AbandonProcessGroup": True,
|
|
217
|
+
"WorkingDirectory": str(HOME),
|
|
218
|
+
"EnvironmentVariables": {"PATH": ":".join(DOCKER_PATHS)},
|
|
219
|
+
"StandardOutPath": str(AUTOSTART_LOG),
|
|
220
|
+
"StandardErrorPath": str(AUTOSTART_LOG),
|
|
221
|
+
}
|
|
222
|
+
if AGENT_PLIST.exists():
|
|
223
|
+
subprocess.run(["launchctl", "bootout", f"gui/{uid}", str(AGENT_PLIST)], capture_output=True)
|
|
224
|
+
AGENT_PLIST.write_bytes(plistlib.dumps(plist))
|
|
225
|
+
r = subprocess.run(["launchctl", "bootstrap", f"gui/{uid}", str(AGENT_PLIST)], capture_output=True, text=True)
|
|
226
|
+
if r.returncode != 0:
|
|
227
|
+
raise SystemExit(f"launchctl rejected the agent: {r.stderr.strip()}")
|
|
228
|
+
print(f"Pythia starts by itself at login (opens Docker Desktop if needed). Agent: {AGENT_PLIST} · log: {AUTOSTART_LOG}")
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def main(argv: list[str] | None = None) -> None:
|
|
232
|
+
parser = argparse.ArgumentParser(prog="pythia", description="Personal knowledge oracle.")
|
|
233
|
+
parser.add_argument("--version", action="version", version=f"pythia {__version__}")
|
|
234
|
+
sub = parser.add_subparsers(dest="cmd", required=True)
|
|
235
|
+
for name, fn in (("start", cmd_start), ("stop", cmd_stop), ("status", cmd_status), ("run", cmd_run),
|
|
236
|
+
("boot", cmd_boot)):
|
|
237
|
+
sub.add_parser(name).set_defaults(fn=fn)
|
|
238
|
+
auto = sub.add_parser("autostart")
|
|
239
|
+
auto.add_argument("--off", action="store_true", help="stop starting by itself at login")
|
|
240
|
+
auto.set_defaults(fn=cmd_autostart)
|
|
241
|
+
args = parser.parse_args(argv)
|
|
242
|
+
args.fn(args)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
if __name__ == "__main__":
|
|
246
|
+
main()
|
|
File without changes
|