prompture 0.0.35__py3-none-any.whl → 0.0.38.dev2__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.
- prompture/__init__.py +120 -2
- prompture/_version.py +2 -2
- prompture/agent.py +924 -0
- prompture/agent_types.py +156 -0
- prompture/async_agent.py +880 -0
- prompture/async_conversation.py +199 -17
- prompture/async_driver.py +24 -0
- prompture/async_groups.py +551 -0
- prompture/conversation.py +213 -18
- prompture/core.py +30 -12
- prompture/discovery.py +24 -1
- prompture/driver.py +38 -0
- prompture/drivers/__init__.py +5 -1
- prompture/drivers/async_azure_driver.py +7 -1
- prompture/drivers/async_claude_driver.py +7 -1
- prompture/drivers/async_google_driver.py +212 -28
- prompture/drivers/async_grok_driver.py +7 -1
- prompture/drivers/async_groq_driver.py +7 -1
- prompture/drivers/async_lmstudio_driver.py +74 -5
- prompture/drivers/async_ollama_driver.py +13 -3
- prompture/drivers/async_openai_driver.py +7 -1
- prompture/drivers/async_openrouter_driver.py +7 -1
- prompture/drivers/async_registry.py +5 -1
- prompture/drivers/azure_driver.py +7 -1
- prompture/drivers/claude_driver.py +7 -1
- prompture/drivers/google_driver.py +217 -33
- prompture/drivers/grok_driver.py +7 -1
- prompture/drivers/groq_driver.py +7 -1
- prompture/drivers/lmstudio_driver.py +73 -8
- prompture/drivers/ollama_driver.py +16 -5
- prompture/drivers/openai_driver.py +7 -1
- prompture/drivers/openrouter_driver.py +7 -1
- prompture/drivers/vision_helpers.py +153 -0
- prompture/group_types.py +147 -0
- prompture/groups.py +530 -0
- prompture/image.py +180 -0
- prompture/persistence.py +254 -0
- prompture/persona.py +482 -0
- prompture/serialization.py +218 -0
- prompture/settings.py +1 -0
- prompture-0.0.38.dev2.dist-info/METADATA +369 -0
- prompture-0.0.38.dev2.dist-info/RECORD +77 -0
- prompture-0.0.35.dist-info/METADATA +0 -464
- prompture-0.0.35.dist-info/RECORD +0 -66
- {prompture-0.0.35.dist-info → prompture-0.0.38.dev2.dist-info}/WHEEL +0 -0
- {prompture-0.0.35.dist-info → prompture-0.0.38.dev2.dist-info}/entry_points.txt +0 -0
- {prompture-0.0.35.dist-info → prompture-0.0.38.dev2.dist-info}/licenses/LICENSE +0 -0
- {prompture-0.0.35.dist-info → prompture-0.0.38.dev2.dist-info}/top_level.txt +0 -0
prompture/persistence.py
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"""Conversation persistence — file and SQLite storage backends.
|
|
2
|
+
|
|
3
|
+
Provides:
|
|
4
|
+
|
|
5
|
+
- :func:`save_to_file` / :func:`load_from_file` for simple JSON file storage.
|
|
6
|
+
- :class:`ConversationStore` for SQLite-backed storage with tag search.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import sqlite3
|
|
13
|
+
import threading
|
|
14
|
+
from datetime import datetime, timezone
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
# ------------------------------------------------------------------
|
|
19
|
+
# File-based persistence
|
|
20
|
+
# ------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def save_to_file(data: dict[str, Any], path: str | Path) -> None:
|
|
24
|
+
"""Write a conversation export dict as JSON to *path*.
|
|
25
|
+
|
|
26
|
+
Creates parent directories if they don't exist.
|
|
27
|
+
"""
|
|
28
|
+
p = Path(path)
|
|
29
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
30
|
+
p.write_text(json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def load_from_file(path: str | Path) -> dict[str, Any]:
|
|
34
|
+
"""Read a conversation export dict from a JSON file.
|
|
35
|
+
|
|
36
|
+
Raises:
|
|
37
|
+
FileNotFoundError: If *path* does not exist.
|
|
38
|
+
"""
|
|
39
|
+
p = Path(path)
|
|
40
|
+
if not p.exists():
|
|
41
|
+
raise FileNotFoundError(f"Conversation file not found: {p}")
|
|
42
|
+
return json.loads(p.read_text(encoding="utf-8"))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# ------------------------------------------------------------------
|
|
46
|
+
# SQLite-backed ConversationStore
|
|
47
|
+
# ------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
_DEFAULT_DB_DIR = Path.home() / ".prompture" / "conversations"
|
|
50
|
+
_DEFAULT_DB_PATH = _DEFAULT_DB_DIR / "conversations.db"
|
|
51
|
+
|
|
52
|
+
_SCHEMA_SQL = """
|
|
53
|
+
CREATE TABLE IF NOT EXISTS conversations (
|
|
54
|
+
id TEXT PRIMARY KEY,
|
|
55
|
+
model_name TEXT NOT NULL,
|
|
56
|
+
data TEXT NOT NULL,
|
|
57
|
+
created_at TEXT NOT NULL,
|
|
58
|
+
last_active TEXT NOT NULL,
|
|
59
|
+
turn_count INTEGER NOT NULL DEFAULT 0
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
CREATE TABLE IF NOT EXISTS conversation_tags (
|
|
63
|
+
conversation_id TEXT NOT NULL,
|
|
64
|
+
tag TEXT NOT NULL,
|
|
65
|
+
PRIMARY KEY (conversation_id, tag),
|
|
66
|
+
FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
CREATE INDEX IF NOT EXISTS idx_tags_tag ON conversation_tags(tag);
|
|
70
|
+
CREATE INDEX IF NOT EXISTS idx_conversations_last_active ON conversations(last_active);
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class ConversationStore:
|
|
75
|
+
"""SQLite-backed conversation storage with tag search.
|
|
76
|
+
|
|
77
|
+
Thread-safe — uses an internal :class:`threading.Lock` for all
|
|
78
|
+
database operations (mirrors the pattern used by ``cache.py``).
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
db_path: Path to the SQLite database file. Defaults to
|
|
82
|
+
``~/.prompture/conversations/conversations.db``.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
def __init__(self, db_path: str | Path | None = None) -> None:
|
|
86
|
+
self._db_path = Path(db_path) if db_path else _DEFAULT_DB_PATH
|
|
87
|
+
self._db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
88
|
+
self._lock = threading.Lock()
|
|
89
|
+
self._init_db()
|
|
90
|
+
|
|
91
|
+
def _init_db(self) -> None:
|
|
92
|
+
with self._lock:
|
|
93
|
+
conn = sqlite3.connect(str(self._db_path))
|
|
94
|
+
try:
|
|
95
|
+
conn.executescript(_SCHEMA_SQL)
|
|
96
|
+
conn.commit()
|
|
97
|
+
finally:
|
|
98
|
+
conn.close()
|
|
99
|
+
|
|
100
|
+
def _connect(self) -> sqlite3.Connection:
|
|
101
|
+
conn = sqlite3.connect(str(self._db_path))
|
|
102
|
+
conn.execute("PRAGMA foreign_keys = ON")
|
|
103
|
+
conn.row_factory = sqlite3.Row
|
|
104
|
+
return conn
|
|
105
|
+
|
|
106
|
+
# ------------------------------------------------------------------ #
|
|
107
|
+
# CRUD
|
|
108
|
+
# ------------------------------------------------------------------ #
|
|
109
|
+
|
|
110
|
+
def save(self, conversation_id: str, data: dict[str, Any]) -> None:
|
|
111
|
+
"""Upsert a conversation and replace its tags."""
|
|
112
|
+
meta = data.get("metadata", {})
|
|
113
|
+
model_name = data.get("model_name", "")
|
|
114
|
+
created_at = meta.get("created_at", datetime.now(timezone.utc).isoformat())
|
|
115
|
+
last_active = meta.get("last_active", datetime.now(timezone.utc).isoformat())
|
|
116
|
+
turn_count = meta.get("turn_count", 0)
|
|
117
|
+
tags = meta.get("tags", [])
|
|
118
|
+
|
|
119
|
+
data_json = json.dumps(data, ensure_ascii=False)
|
|
120
|
+
|
|
121
|
+
with self._lock:
|
|
122
|
+
conn = self._connect()
|
|
123
|
+
try:
|
|
124
|
+
conn.execute(
|
|
125
|
+
"""
|
|
126
|
+
INSERT INTO conversations (id, model_name, data, created_at, last_active, turn_count)
|
|
127
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
128
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
129
|
+
model_name = excluded.model_name,
|
|
130
|
+
data = excluded.data,
|
|
131
|
+
last_active = excluded.last_active,
|
|
132
|
+
turn_count = excluded.turn_count
|
|
133
|
+
""",
|
|
134
|
+
(conversation_id, model_name, data_json, created_at, last_active, turn_count),
|
|
135
|
+
)
|
|
136
|
+
# Replace tags
|
|
137
|
+
conn.execute(
|
|
138
|
+
"DELETE FROM conversation_tags WHERE conversation_id = ?",
|
|
139
|
+
(conversation_id,),
|
|
140
|
+
)
|
|
141
|
+
if tags:
|
|
142
|
+
conn.executemany(
|
|
143
|
+
"INSERT INTO conversation_tags (conversation_id, tag) VALUES (?, ?)",
|
|
144
|
+
[(conversation_id, t) for t in tags],
|
|
145
|
+
)
|
|
146
|
+
conn.commit()
|
|
147
|
+
finally:
|
|
148
|
+
conn.close()
|
|
149
|
+
|
|
150
|
+
def load(self, conversation_id: str) -> dict[str, Any] | None:
|
|
151
|
+
"""Load a conversation by ID. Returns ``None`` if not found."""
|
|
152
|
+
with self._lock:
|
|
153
|
+
conn = self._connect()
|
|
154
|
+
try:
|
|
155
|
+
row = conn.execute(
|
|
156
|
+
"SELECT data FROM conversations WHERE id = ?",
|
|
157
|
+
(conversation_id,),
|
|
158
|
+
).fetchone()
|
|
159
|
+
if row is None:
|
|
160
|
+
return None
|
|
161
|
+
return json.loads(row["data"])
|
|
162
|
+
finally:
|
|
163
|
+
conn.close()
|
|
164
|
+
|
|
165
|
+
def delete(self, conversation_id: str) -> bool:
|
|
166
|
+
"""Delete a conversation. Returns *True* if it existed."""
|
|
167
|
+
with self._lock:
|
|
168
|
+
conn = self._connect()
|
|
169
|
+
try:
|
|
170
|
+
cursor = conn.execute(
|
|
171
|
+
"DELETE FROM conversations WHERE id = ?",
|
|
172
|
+
(conversation_id,),
|
|
173
|
+
)
|
|
174
|
+
conn.commit()
|
|
175
|
+
return cursor.rowcount > 0
|
|
176
|
+
finally:
|
|
177
|
+
conn.close()
|
|
178
|
+
|
|
179
|
+
# ------------------------------------------------------------------ #
|
|
180
|
+
# Search / listing
|
|
181
|
+
# ------------------------------------------------------------------ #
|
|
182
|
+
|
|
183
|
+
def find_by_tag(self, tag: str) -> list[dict[str, Any]]:
|
|
184
|
+
"""Return summary dicts for all conversations with the given tag."""
|
|
185
|
+
with self._lock:
|
|
186
|
+
conn = self._connect()
|
|
187
|
+
try:
|
|
188
|
+
rows = conn.execute(
|
|
189
|
+
"""
|
|
190
|
+
SELECT c.id, c.model_name, c.created_at, c.last_active, c.turn_count
|
|
191
|
+
FROM conversations c
|
|
192
|
+
INNER JOIN conversation_tags ct ON c.id = ct.conversation_id
|
|
193
|
+
WHERE ct.tag = ?
|
|
194
|
+
ORDER BY c.last_active DESC
|
|
195
|
+
""",
|
|
196
|
+
(tag,),
|
|
197
|
+
).fetchall()
|
|
198
|
+
return [self._row_to_summary(conn, r) for r in rows]
|
|
199
|
+
finally:
|
|
200
|
+
conn.close()
|
|
201
|
+
|
|
202
|
+
def find_by_id(self, conversation_id: str) -> dict[str, Any] | None:
|
|
203
|
+
"""Return a summary dict (with tags) for a conversation, or ``None``."""
|
|
204
|
+
with self._lock:
|
|
205
|
+
conn = self._connect()
|
|
206
|
+
try:
|
|
207
|
+
row = conn.execute(
|
|
208
|
+
"SELECT id, model_name, created_at, last_active, turn_count FROM conversations WHERE id = ?",
|
|
209
|
+
(conversation_id,),
|
|
210
|
+
).fetchone()
|
|
211
|
+
if row is None:
|
|
212
|
+
return None
|
|
213
|
+
return self._row_to_summary(conn, row)
|
|
214
|
+
finally:
|
|
215
|
+
conn.close()
|
|
216
|
+
|
|
217
|
+
def list_all(self, limit: int = 100, offset: int = 0) -> list[dict[str, Any]]:
|
|
218
|
+
"""Return summary dicts ordered by ``last_active`` descending."""
|
|
219
|
+
with self._lock:
|
|
220
|
+
conn = self._connect()
|
|
221
|
+
try:
|
|
222
|
+
rows = conn.execute(
|
|
223
|
+
"""
|
|
224
|
+
SELECT id, model_name, created_at, last_active, turn_count
|
|
225
|
+
FROM conversations
|
|
226
|
+
ORDER BY last_active DESC
|
|
227
|
+
LIMIT ? OFFSET ?
|
|
228
|
+
""",
|
|
229
|
+
(limit, offset),
|
|
230
|
+
).fetchall()
|
|
231
|
+
return [self._row_to_summary(conn, r) for r in rows]
|
|
232
|
+
finally:
|
|
233
|
+
conn.close()
|
|
234
|
+
|
|
235
|
+
# ------------------------------------------------------------------ #
|
|
236
|
+
# Internal
|
|
237
|
+
# ------------------------------------------------------------------ #
|
|
238
|
+
|
|
239
|
+
@staticmethod
|
|
240
|
+
def _row_to_summary(conn: sqlite3.Connection, row: sqlite3.Row) -> dict[str, Any]:
|
|
241
|
+
"""Build a summary dict from a DB row, including tags."""
|
|
242
|
+
cid = row["id"]
|
|
243
|
+
tag_rows = conn.execute(
|
|
244
|
+
"SELECT tag FROM conversation_tags WHERE conversation_id = ?",
|
|
245
|
+
(cid,),
|
|
246
|
+
).fetchall()
|
|
247
|
+
return {
|
|
248
|
+
"id": cid,
|
|
249
|
+
"model_name": row["model_name"],
|
|
250
|
+
"created_at": row["created_at"],
|
|
251
|
+
"last_active": row["last_active"],
|
|
252
|
+
"turn_count": row["turn_count"],
|
|
253
|
+
"tags": [tr["tag"] for tr in tag_rows],
|
|
254
|
+
}
|