polymath-agent 0.4.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 (54) hide show
  1. polymath/__init__.py +2 -0
  2. polymath/adapters/__init__.py +7 -0
  3. polymath/adapters/base.py +175 -0
  4. polymath/adapters/claude.py +280 -0
  5. polymath/adapters/gemini.py +186 -0
  6. polymath/adapters/ollama.py +117 -0
  7. polymath/adapters/openai_adapter.py +168 -0
  8. polymath/bootstrap.py +159 -0
  9. polymath/command_registry.py +41 -0
  10. polymath/command_service.py +572 -0
  11. polymath/compressor.py +90 -0
  12. polymath/config.py +293 -0
  13. polymath/context_manager.py +76 -0
  14. polymath/context_store.py +336 -0
  15. polymath/detector.py +442 -0
  16. polymath/domain.py +78 -0
  17. polymath/execution_service.py +325 -0
  18. polymath/main.py +1293 -0
  19. polymath/memory/__init__.py +15 -0
  20. polymath/memory/chunker.py +6 -0
  21. polymath/memory/embedder.py +179 -0
  22. polymath/memory/migrate.py +2 -0
  23. polymath/memory/retriever.py +2 -0
  24. polymath/memory/store.py +9 -0
  25. polymath/memory/sync.py +2 -0
  26. polymath/memory/writer.py +9 -0
  27. polymath/model_policy.py +172 -0
  28. polymath/orchestrator/__init__.py +68 -0
  29. polymath/orchestrator/attempt_ledger.py +34 -0
  30. polymath/orchestrator/ensemble.py +229 -0
  31. polymath/orchestrator/fanout.py +322 -0
  32. polymath/orchestrator/output_policy.py +61 -0
  33. polymath/orchestrator/race.py +311 -0
  34. polymath/orchestrator/run_controller.py +91 -0
  35. polymath/orchestrator/speculative_review.py +120 -0
  36. polymath/orchestrator/state_responder.py +184 -0
  37. polymath/orchestrator/worker_pool.py +37 -0
  38. polymath/permissions.py +82 -0
  39. polymath/pipeline.py +700 -0
  40. polymath/project_config.py +229 -0
  41. polymath/project_runtime.py +109 -0
  42. polymath/router.py +127 -0
  43. polymath/setup_wizard.py +106 -0
  44. polymath/slash_commands.py +566 -0
  45. polymath/subagents.py +486 -0
  46. polymath/tools.py +333 -0
  47. polymath/ui_state.py +84 -0
  48. polymath/workspace.py +66 -0
  49. polymath_agent-0.4.0.dist-info/METADATA +693 -0
  50. polymath_agent-0.4.0.dist-info/RECORD +54 -0
  51. polymath_agent-0.4.0.dist-info/WHEEL +5 -0
  52. polymath_agent-0.4.0.dist-info/entry_points.txt +2 -0
  53. polymath_agent-0.4.0.dist-info/licenses/LICENSE +21 -0
  54. polymath_agent-0.4.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,336 @@
1
+ """
2
+ SQLite-backed unified context store.
3
+ All models share one thread per session.
4
+ """
5
+ from __future__ import annotations
6
+
7
+ import sqlite3
8
+ import uuid
9
+ from contextlib import contextmanager
10
+ from datetime import datetime, timezone, timedelta
11
+ from pathlib import Path
12
+
13
+ from polymath.config import DB_FILE
14
+ from polymath.adapters.base import Message
15
+
16
+ # Single persistent connection — WAL mode for async safety, timeout so
17
+ # writers wait instead of immediately raising OperationalError: database is locked
18
+ _db_conn: sqlite3.Connection | None = None
19
+
20
+
21
+ def _conn() -> sqlite3.Connection:
22
+ global _db_conn
23
+ DB_FILE.parent.mkdir(parents=True, exist_ok=True)
24
+ if _db_conn is None:
25
+ _db_conn = sqlite3.connect(
26
+ str(DB_FILE),
27
+ timeout=30,
28
+ check_same_thread=False,
29
+ )
30
+ _db_conn.row_factory = sqlite3.Row
31
+ _db_conn.execute("PRAGMA journal_mode=WAL")
32
+ _db_conn.execute("PRAGMA synchronous=NORMAL")
33
+ _db_conn.commit()
34
+ return _db_conn
35
+
36
+
37
+ def init_db() -> None:
38
+ with _conn() as conn:
39
+ conn.executescript("""
40
+ CREATE TABLE IF NOT EXISTS sessions (
41
+ id TEXT PRIMARY KEY,
42
+ title TEXT,
43
+ created_at TEXT,
44
+ updated_at TEXT,
45
+ project TEXT DEFAULT ''
46
+ );
47
+ -- migrate: add project column if upgrading from older db
48
+ CREATE TABLE IF NOT EXISTS _migrations (key TEXT PRIMARY KEY);
49
+
50
+
51
+ CREATE TABLE IF NOT EXISTS messages (
52
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
53
+ session_id TEXT NOT NULL,
54
+ role TEXT NOT NULL,
55
+ content TEXT NOT NULL,
56
+ model TEXT DEFAULT '',
57
+ step TEXT DEFAULT '',
58
+ token_count INTEGER DEFAULT 0,
59
+ created_at TEXT NOT NULL,
60
+ FOREIGN KEY (session_id) REFERENCES sessions(id)
61
+ );
62
+
63
+ CREATE TABLE IF NOT EXISTS summaries (
64
+ session_id TEXT PRIMARY KEY,
65
+ summary TEXT NOT NULL,
66
+ summarized_through INTEGER,
67
+ updated_at TEXT,
68
+ FOREIGN KEY (session_id) REFERENCES sessions(id)
69
+ );
70
+ """)
71
+ # Safe migration for existing databases
72
+ try:
73
+ conn.execute("ALTER TABLE sessions ADD COLUMN project TEXT DEFAULT ''")
74
+ except Exception:
75
+ pass # column already exists
76
+
77
+
78
+ # ── Sessions ──────────────────────────────────────────────────────────────────
79
+
80
+ def new_session(title: str = "") -> str:
81
+ sid = str(uuid.uuid4())[:8]
82
+ now = datetime.now(timezone.utc).isoformat()
83
+ with _conn() as conn:
84
+ conn.execute(
85
+ "INSERT INTO sessions (id, title, created_at, updated_at) VALUES (?,?,?,?)",
86
+ (sid, title or f"Session {sid}", now, now),
87
+ )
88
+ return sid
89
+
90
+
91
+ def list_sessions() -> list[dict]:
92
+ with _conn() as conn:
93
+ rows = conn.execute(
94
+ "SELECT id, title, created_at, updated_at FROM sessions ORDER BY updated_at DESC LIMIT 20"
95
+ ).fetchall()
96
+ return [dict(r) for r in rows]
97
+
98
+
99
+ def get_session(sid: str) -> dict | None:
100
+ with _conn() as conn:
101
+ row = conn.execute("SELECT * FROM sessions WHERE id=?", (sid,)).fetchone()
102
+ return dict(row) if row else None
103
+
104
+
105
+ def touch_session(sid: str, title: str = "") -> None:
106
+ now = datetime.now(timezone.utc).isoformat()
107
+ with _conn() as conn:
108
+ if title:
109
+ conn.execute("UPDATE sessions SET updated_at=?, title=? WHERE id=?", (now, title, sid))
110
+ else:
111
+ conn.execute("UPDATE sessions SET updated_at=? WHERE id=?", (now, sid))
112
+
113
+
114
+ def set_session_project(sid: str, project: str) -> None:
115
+ """Tag an existing session to a project."""
116
+ now = datetime.now(timezone.utc).isoformat()
117
+ with _conn() as conn:
118
+ conn.execute(
119
+ "UPDATE sessions SET project=?, updated_at=? WHERE id=?",
120
+ (project, now, sid),
121
+ )
122
+
123
+
124
+ # ── Messages ──────────────────────────────────────────────────────────────────
125
+
126
+ def add_message(
127
+ session_id: str,
128
+ role: str,
129
+ content: str,
130
+ model: str = "",
131
+ step: str = "",
132
+ token_count: int = 0,
133
+ ) -> int:
134
+ now = datetime.now(timezone.utc).isoformat()
135
+ with _conn() as conn:
136
+ cur = conn.execute(
137
+ "INSERT INTO messages (session_id,role,content,model,step,token_count,created_at) VALUES (?,?,?,?,?,?,?)",
138
+ (session_id, role, content, model, step, token_count, now),
139
+ )
140
+ touch_session(session_id)
141
+ return cur.lastrowid
142
+
143
+
144
+ def get_messages(session_id: str, limit: int = 0) -> list[Message]:
145
+ with _conn() as conn:
146
+ if limit:
147
+ rows = conn.execute(
148
+ "SELECT * FROM messages WHERE session_id=? ORDER BY id DESC LIMIT ?",
149
+ (session_id, limit),
150
+ ).fetchall()
151
+ rows = list(reversed(rows))
152
+ else:
153
+ rows = conn.execute(
154
+ "SELECT * FROM messages WHERE session_id=? ORDER BY id ASC",
155
+ (session_id,),
156
+ ).fetchall()
157
+ return [Message(role=r["role"], content=r["content"], model=r["model"], step=r["step"]) for r in rows]
158
+
159
+
160
+ def get_all_messages_raw(session_id: str) -> list[dict]:
161
+ with _conn() as conn:
162
+ rows = conn.execute(
163
+ "SELECT * FROM messages WHERE session_id=? ORDER BY id ASC", (session_id,)
164
+ ).fetchall()
165
+ return [dict(r) for r in rows]
166
+
167
+
168
+ def last_message_id(session_id: str) -> int:
169
+ with _conn() as conn:
170
+ row = conn.execute(
171
+ "SELECT MAX(id) as max_id FROM messages WHERE session_id=?", (session_id,)
172
+ ).fetchone()
173
+ return row["max_id"] or 0
174
+
175
+
176
+ # ── Summaries ─────────────────────────────────────────────────────────────────
177
+
178
+ def get_summary(session_id: str) -> dict | None:
179
+ with _conn() as conn:
180
+ row = conn.execute("SELECT * FROM summaries WHERE session_id=?", (session_id,)).fetchone()
181
+ return dict(row) if row else None
182
+
183
+
184
+ def save_summary(session_id: str, summary: str, through_id: int) -> None:
185
+ now = datetime.now(timezone.utc).isoformat()
186
+ with _conn() as conn:
187
+ conn.execute(
188
+ """INSERT INTO summaries (session_id, summary, summarized_through, updated_at)
189
+ VALUES (?,?,?,?)
190
+ ON CONFLICT(session_id) DO UPDATE SET
191
+ summary=excluded.summary,
192
+ summarized_through=excluded.summarized_through,
193
+ updated_at=excluded.updated_at""",
194
+ (session_id, summary, through_id, now),
195
+ )
196
+
197
+
198
+ def auto_name_session(session_id: str, first_message: str) -> None:
199
+ """Generate a human-readable session name from the first user message."""
200
+ words = first_message.strip().split()
201
+ name = " ".join(words[:6])
202
+ if len(words) > 6:
203
+ name += "..."
204
+ touch_session(session_id, title=name)
205
+
206
+
207
+ def get_recent_sessions(project: str = "", limit: int = 10) -> list[dict]:
208
+ """Return sessions ordered by last active, optionally filtered by project."""
209
+ with _conn() as conn:
210
+ if project:
211
+ rows = conn.execute(
212
+ "SELECT * FROM sessions WHERE project=? ORDER BY updated_at DESC LIMIT ?",
213
+ (project, limit),
214
+ ).fetchall()
215
+ else:
216
+ rows = conn.execute(
217
+ "SELECT * FROM sessions ORDER BY updated_at DESC LIMIT ?",
218
+ (limit,),
219
+ ).fetchall()
220
+ return [dict(r) for r in rows]
221
+
222
+
223
+ def get_last_session_for_project(project: str) -> dict | None:
224
+ """Return the most recently active session tagged to a project."""
225
+ with _conn() as conn:
226
+ row = conn.execute(
227
+ "SELECT * FROM sessions WHERE project=? ORDER BY updated_at DESC LIMIT 1",
228
+ (project,),
229
+ ).fetchone()
230
+ return dict(row) if row else None
231
+
232
+
233
+ def was_recent(session: dict, hours: int = 24) -> bool:
234
+ """Return True if session was active within the last N hours."""
235
+ try:
236
+ updated = datetime.fromisoformat(session["updated_at"])
237
+ if updated.tzinfo is None:
238
+ updated = updated.replace(tzinfo=timezone.utc)
239
+ return datetime.now(timezone.utc) - updated < timedelta(hours=hours)
240
+ except Exception:
241
+ return False
242
+
243
+
244
+ def time_ago(session: dict) -> str:
245
+ """Human-readable time since last activity."""
246
+ try:
247
+ updated = datetime.fromisoformat(session["updated_at"])
248
+ if updated.tzinfo is None:
249
+ updated = updated.replace(tzinfo=timezone.utc)
250
+ diff = datetime.now(timezone.utc) - updated
251
+ minutes = int(diff.total_seconds() / 60)
252
+ if minutes < 60:
253
+ return f"{minutes}m ago"
254
+ hours = minutes // 60
255
+ if hours < 24:
256
+ return f"{hours}h ago"
257
+ days = hours // 24
258
+ return f"{days}d ago"
259
+ except Exception:
260
+ return "unknown"
261
+
262
+
263
+ def new_session_for_project(project: str) -> str:
264
+ """Create a new session tagged to a project."""
265
+ sid = str(uuid.uuid4())[:8]
266
+ now = datetime.now(timezone.utc).isoformat()
267
+ with _conn() as conn:
268
+ conn.execute(
269
+ "INSERT INTO sessions (id, title, created_at, updated_at, project) VALUES (?,?,?,?,?)",
270
+ (sid, f"Session {sid}", now, now, project),
271
+ )
272
+ return sid
273
+
274
+
275
+ def session_cost(session_id: str) -> tuple[int, float]:
276
+ """Returns (total_tokens, estimated_usd). Rough estimate only."""
277
+ msgs = get_all_messages_raw(session_id)
278
+ total_tokens = sum(m.get("token_count", 0) for m in msgs)
279
+ usd = total_tokens * 0.003 / 1000
280
+ return total_tokens, usd
281
+
282
+
283
+ def delete_session(session_id: str) -> bool:
284
+ """Delete a session and all its messages. Returns True if found and deleted."""
285
+ with _conn() as conn:
286
+ row = conn.execute("SELECT id FROM sessions WHERE id=?", (session_id,)).fetchone()
287
+ if not row:
288
+ return False
289
+ conn.execute("DELETE FROM messages WHERE session_id=?", (session_id,))
290
+ conn.execute("DELETE FROM summaries WHERE session_id=?", (session_id,))
291
+ conn.execute("DELETE FROM sessions WHERE id=?", (session_id,))
292
+ return True
293
+
294
+
295
+ def export_session_md(session_id: str) -> str:
296
+ """Export session to a Markdown file in SESSIONS_DIR and return the path."""
297
+ from polymath.config import SESSIONS_DIR
298
+ SESSIONS_DIR.mkdir(parents=True, exist_ok=True)
299
+
300
+ session = get_session(session_id)
301
+ messages = get_messages(session_id)
302
+ if not session:
303
+ return ""
304
+
305
+ filename = f"{session_id}_{session['title'].replace(' ', '_')}.md"
306
+ file_path = SESSIONS_DIR / filename
307
+
308
+ lines = [
309
+ f"# {session['title']}",
310
+ f"Session ID: {session_id}",
311
+ f"Created: {session['created_at']}",
312
+ f"Updated: {session['updated_at']}",
313
+ "\n---\n",
314
+ ]
315
+
316
+ for m in messages:
317
+ role = m.role.upper()
318
+ if m.model:
319
+ role = f"{role} ({m.model})"
320
+
321
+ lines.append(f"### {role}")
322
+ if m.step:
323
+ lines.append(f"*Step: {m.step}*")
324
+
325
+ lines.append(f"\n{m.content}\n")
326
+
327
+ if m.tool_calls:
328
+ lines.append("#### Tool Calls:")
329
+ for tc in m.tool_calls:
330
+ lines.append(f"- `{tc.name}({tc.arguments})`")
331
+ lines.append("\n")
332
+
333
+ with open(file_path, "w") as f:
334
+ f.write("\n".join(lines))
335
+
336
+ return str(file_path)