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.
Files changed (64) hide show
  1. pythia/__init__.py +3 -0
  2. pythia/cli.py +246 -0
  3. pythia/connectors/__init__.py +0 -0
  4. pythia/connectors/claude_code.py +467 -0
  5. pythia/connectors/common.py +99 -0
  6. pythia/connectors/confluence.py +803 -0
  7. pythia/connectors/github.py +444 -0
  8. pythia/connectors/incident_io.py +365 -0
  9. pythia/connectors/slack.py +674 -0
  10. pythia/consumer.py +268 -0
  11. pythia/daemon.py +650 -0
  12. pythia/db.py +80 -0
  13. pythia/docker-compose.yml +15 -0
  14. pythia/embed.py +39 -0
  15. pythia/events.py +339 -0
  16. pythia/evolution.py +392 -0
  17. pythia/ids.py +12 -0
  18. pythia/jobs.py +103 -0
  19. pythia/knowledge.py +494 -0
  20. pythia/llm/__init__.py +0 -0
  21. pythia/llm/client.py +328 -0
  22. pythia/mcp_server.py +73 -0
  23. pythia/migrations/001_schema.sql +536 -0
  24. pythia/migrations/__init__.py +0 -0
  25. pythia/pipeline/__init__.py +0 -0
  26. pythia/pipeline/adjudicate.py +297 -0
  27. pythia/pipeline/batch.py +254 -0
  28. pythia/pipeline/correct.py +227 -0
  29. pythia/pipeline/derive.py +317 -0
  30. pythia/pipeline/live.py +149 -0
  31. pythia/pipeline/process.py +472 -0
  32. pythia/pipeline/prompts.py +260 -0
  33. pythia/pipeline/reconcile.py +168 -0
  34. pythia/pipeline/schemas.py +154 -0
  35. pythia/pipeline/sweep.py +155 -0
  36. pythia/quality.py +359 -0
  37. pythia/reexplain.py +248 -0
  38. pythia/retrieval.py +137 -0
  39. pythia/services.py +41 -0
  40. pythia/settings.py +194 -0
  41. pythia/usage.py +135 -0
  42. pythia/value.py +265 -0
  43. pythia/vault.py +31 -0
  44. pythia/web/__init__.py +0 -0
  45. pythia/web/app.py +779 -0
  46. pythia/web/graph.py +167 -0
  47. pythia/web/processing.py +131 -0
  48. pythia/web/static/explore.js +242 -0
  49. pythia/web/static/style.css +120 -0
  50. pythia/web/templates/_now.html +25 -0
  51. pythia/web/templates/base.html +30 -0
  52. pythia/web/templates/config.html +231 -0
  53. pythia/web/templates/costs.html +30 -0
  54. pythia/web/templates/evolution.html +153 -0
  55. pythia/web/templates/explore.html +36 -0
  56. pythia/web/templates/label.html +22 -0
  57. pythia/web/templates/quality.html +141 -0
  58. pythia/web/templates/sources.html +249 -0
  59. pythia/web/templates/usage.html +33 -0
  60. pythia_knowledge-1.0.0.dist-info/METADATA +197 -0
  61. pythia_knowledge-1.0.0.dist-info/RECORD +64 -0
  62. pythia_knowledge-1.0.0.dist-info/WHEEL +4 -0
  63. pythia_knowledge-1.0.0.dist-info/entry_points.txt +2 -0
  64. pythia_knowledge-1.0.0.dist-info/licenses/LICENSE +21 -0
pythia/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """Pythia — personal knowledge oracle."""
2
+
3
+ __version__ = "1.0.0"
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