seekdb-cli 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.
seekdb_cli/__init__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
File without changes
@@ -0,0 +1,306 @@
1
+ """seekdb ai command — use database AI (DBMS_AI_SERVICE, AI_COMPLETE)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+
7
+ import click
8
+ import pymysql
9
+
10
+ from seekdb_cli import output
11
+ from seekdb_cli.connection import get_connection
12
+ from seekdb_cli.logger import log_operation
13
+
14
+ # OceanBase AI model types (for CREATE_AI_MODEL)
15
+ AI_MODEL_TYPES = ("dense_embedding", "completion", "rerank")
16
+
17
+
18
+ @click.group("ai")
19
+ @click.pass_context
20
+ def ai_cmd(ctx: click.Context) -> None:
21
+ """AI model management and completion (via database DBMS_AI_SERVICE)."""
22
+
23
+
24
+ # ---------------------------------------------------------------------------
25
+ # seekdb ai model ...
26
+ # ---------------------------------------------------------------------------
27
+
28
+ @ai_cmd.group("model")
29
+ @click.pass_context
30
+ def model_group(ctx: click.Context) -> None:
31
+ """Manage AI models registered in the database (DBMS_AI_SERVICE)."""
32
+
33
+
34
+ @model_group.command("list")
35
+ @click.pass_context
36
+ def model_list(ctx: click.Context) -> None:
37
+ """List all registered AI models (from oceanbase.DBA_OB_AI_MODELS)."""
38
+ fmt: str = ctx.obj["format"]
39
+ dsn: str | None = ctx.obj["dsn"]
40
+
41
+ try:
42
+ conn = get_connection(dsn)
43
+ except Exception as exc:
44
+ log_operation("ai model list", ok=False, error_code="CONNECTION_ERROR")
45
+ output.error("CONNECTION_ERROR", str(exc), fmt=fmt)
46
+ return
47
+
48
+ timer = output.Timer()
49
+ try:
50
+ with timer, conn.cursor() as cur:
51
+ cur.execute(
52
+ "SELECT MODEL_ID, NAME, TYPE, MODEL_NAME "
53
+ "FROM oceanbase.DBA_OB_AI_MODELS ORDER BY NAME"
54
+ )
55
+ rows = cur.fetchall()
56
+ # DictCursor: keys may be uppercase from view
57
+ result = []
58
+ for r in rows:
59
+ row = dict(r)
60
+ result.append({
61
+ "name": row.get("NAME") or row.get("name"),
62
+ "type": row.get("TYPE") or row.get("type"),
63
+ "model_name": row.get("MODEL_NAME") or row.get("model_name"),
64
+ "model_id": row.get("MODEL_ID") or row.get("model_id"),
65
+ })
66
+
67
+ log_operation("ai model list", ok=True, time_ms=timer.elapsed_ms)
68
+ output.success(result, time_ms=timer.elapsed_ms, fmt=fmt)
69
+ except pymysql.err.ProgrammingError as exc:
70
+ log_operation("ai model list", ok=False, error_code="SQL_ERROR")
71
+ output.error(
72
+ "SQL_ERROR",
73
+ str(exc) + ". Ensure the database supports DBA_OB_AI_MODELS (OceanBase AI).",
74
+ fmt=fmt,
75
+ )
76
+ except pymysql.err.Error as exc:
77
+ log_operation("ai model list", ok=False, error_code="SQL_ERROR")
78
+ output.error("SQL_ERROR", str(exc), fmt=fmt)
79
+ finally:
80
+ conn.close()
81
+
82
+
83
+ @model_group.command("create")
84
+ @click.argument("name")
85
+ @click.option(
86
+ "--type",
87
+ "model_type",
88
+ type=click.Choice(AI_MODEL_TYPES),
89
+ required=True,
90
+ help="Model type: dense_embedding, completion, or rerank.",
91
+ )
92
+ @click.option(
93
+ "--model",
94
+ "provider_model_name",
95
+ required=True,
96
+ help="Provider model name (e.g. BAAI/bge-m3, THUDM/GLM-4-9B-0414).",
97
+ )
98
+ @click.pass_context
99
+ def model_create(
100
+ ctx: click.Context,
101
+ name: str,
102
+ model_type: str,
103
+ provider_model_name: str,
104
+ ) -> None:
105
+ """Register an AI model via DBMS_AI_SERVICE.CREATE_AI_MODEL. Create an endpoint separately to use it."""
106
+ fmt: str = ctx.obj["format"]
107
+ dsn: str | None = ctx.obj["dsn"]
108
+
109
+ try:
110
+ conn = get_connection(dsn)
111
+ except Exception as exc:
112
+ log_operation("ai model create", ok=False, error_code="CONNECTION_ERROR")
113
+ output.error("CONNECTION_ERROR", str(exc), fmt=fmt)
114
+ return
115
+
116
+ config = json.dumps({"type": model_type, "model_name": provider_model_name})
117
+ timer = output.Timer()
118
+ try:
119
+ with timer, conn.cursor() as cur:
120
+ cur.execute(
121
+ "CALL DBMS_AI_SERVICE.CREATE_AI_MODEL(%s, %s)",
122
+ (name, config),
123
+ )
124
+ conn.commit()
125
+
126
+ log_operation("ai model create", ok=True, time_ms=timer.elapsed_ms)
127
+ output.success(
128
+ {"name": name, "type": model_type, "model_name": provider_model_name},
129
+ time_ms=timer.elapsed_ms,
130
+ fmt=fmt,
131
+ )
132
+ except pymysql.err.Error as exc:
133
+ log_operation("ai model create", ok=False, error_code="SQL_ERROR")
134
+ output.error("SQL_ERROR", str(exc), fmt=fmt)
135
+ finally:
136
+ conn.close()
137
+
138
+
139
+ @model_group.command("delete")
140
+ @click.argument("name")
141
+ @click.pass_context
142
+ def model_delete(ctx: click.Context, name: str) -> None:
143
+ """Drop an AI model via DBMS_AI_SERVICE.DROP_AI_MODEL. Drop endpoints first if any."""
144
+ fmt: str = ctx.obj["format"]
145
+ dsn: str | None = ctx.obj["dsn"]
146
+
147
+ try:
148
+ conn = get_connection(dsn)
149
+ except Exception as exc:
150
+ log_operation("ai model delete", ok=False, error_code="CONNECTION_ERROR")
151
+ output.error("CONNECTION_ERROR", str(exc), fmt=fmt)
152
+ return
153
+
154
+ timer = output.Timer()
155
+ try:
156
+ with timer, conn.cursor() as cur:
157
+ cur.execute("CALL DBMS_AI_SERVICE.DROP_AI_MODEL(%s)", (name,))
158
+ conn.commit()
159
+
160
+ log_operation("ai model delete", ok=True, time_ms=timer.elapsed_ms)
161
+ output.success({"deleted": name}, time_ms=timer.elapsed_ms, fmt=fmt)
162
+ except pymysql.err.Error as exc:
163
+ log_operation("ai model delete", ok=False, error_code="SQL_ERROR")
164
+ output.error("SQL_ERROR", str(exc), fmt=fmt)
165
+ finally:
166
+ conn.close()
167
+
168
+
169
+ # ---------------------------------------------------------------------------
170
+ # seekdb ai model endpoint ...
171
+ # ---------------------------------------------------------------------------
172
+
173
+ @model_group.group("endpoint")
174
+ @click.pass_context
175
+ def endpoint_group(ctx: click.Context) -> None:
176
+ """Create or delete AI model endpoints (DBMS_AI_SERVICE)."""
177
+
178
+
179
+ @endpoint_group.command("create")
180
+ @click.argument("endpoint_name")
181
+ @click.argument("ai_model_name")
182
+ @click.option("--url", required=True, help="API URL (e.g. https://api.siliconflow.cn/v1/chat/completions).")
183
+ @click.option("--access-key", required=True, help="API key for the service.")
184
+ @click.option("--provider", default="siliconflow", help="Provider name (siliconflow, openai, dashscope, etc.).")
185
+ @click.pass_context
186
+ def endpoint_create(
187
+ ctx: click.Context,
188
+ endpoint_name: str,
189
+ ai_model_name: str,
190
+ url: str,
191
+ access_key: str,
192
+ provider: str,
193
+ ) -> None:
194
+ """Create an AI model endpoint via DBMS_AI_SERVICE.CREATE_AI_MODEL_ENDPOINT."""
195
+ fmt: str = ctx.obj["format"]
196
+ dsn: str | None = ctx.obj["dsn"]
197
+
198
+ try:
199
+ conn = get_connection(dsn)
200
+ except Exception as exc:
201
+ log_operation("ai model endpoint create", ok=False, error_code="CONNECTION_ERROR")
202
+ output.error("CONNECTION_ERROR", str(exc), fmt=fmt)
203
+ return
204
+
205
+ config = json.dumps({
206
+ "ai_model_name": ai_model_name,
207
+ "url": url,
208
+ "access_key": access_key,
209
+ "provider": provider,
210
+ })
211
+ timer = output.Timer()
212
+ try:
213
+ with timer, conn.cursor() as cur:
214
+ cur.execute(
215
+ "CALL DBMS_AI_SERVICE.CREATE_AI_MODEL_ENDPOINT(%s, %s)",
216
+ (endpoint_name, config),
217
+ )
218
+ conn.commit()
219
+
220
+ log_operation("ai model endpoint create", ok=True, time_ms=timer.elapsed_ms)
221
+ output.success(
222
+ {"endpoint": endpoint_name, "ai_model": ai_model_name, "provider": provider},
223
+ time_ms=timer.elapsed_ms,
224
+ fmt=fmt,
225
+ )
226
+ except pymysql.err.Error as exc:
227
+ log_operation("ai model endpoint create", ok=False, error_code="SQL_ERROR")
228
+ output.error("SQL_ERROR", str(exc), fmt=fmt)
229
+ finally:
230
+ conn.close()
231
+
232
+
233
+ @endpoint_group.command("delete")
234
+ @click.argument("endpoint_name")
235
+ @click.pass_context
236
+ def endpoint_delete(ctx: click.Context, endpoint_name: str) -> None:
237
+ """Drop an AI model endpoint via DBMS_AI_SERVICE.DROP_AI_MODEL_ENDPOINT."""
238
+ fmt: str = ctx.obj["format"]
239
+ dsn: str | None = ctx.obj["dsn"]
240
+
241
+ try:
242
+ conn = get_connection(dsn)
243
+ except Exception as exc:
244
+ log_operation("ai model endpoint delete", ok=False, error_code="CONNECTION_ERROR")
245
+ output.error("CONNECTION_ERROR", str(exc), fmt=fmt)
246
+ return
247
+
248
+ timer = output.Timer()
249
+ try:
250
+ with timer, conn.cursor() as cur:
251
+ cur.execute(
252
+ "CALL DBMS_AI_SERVICE.DROP_AI_MODEL_ENDPOINT(%s)",
253
+ (endpoint_name,),
254
+ )
255
+ conn.commit()
256
+
257
+ log_operation("ai model endpoint delete", ok=True, time_ms=timer.elapsed_ms)
258
+ output.success({"deleted": endpoint_name}, time_ms=timer.elapsed_ms, fmt=fmt)
259
+ except pymysql.err.Error as exc:
260
+ log_operation("ai model endpoint delete", ok=False, error_code="SQL_ERROR")
261
+ output.error("SQL_ERROR", str(exc), fmt=fmt)
262
+ finally:
263
+ conn.close()
264
+
265
+
266
+ # ---------------------------------------------------------------------------
267
+ # seekdb ai complete ...
268
+ # ---------------------------------------------------------------------------
269
+
270
+ @ai_cmd.command("complete")
271
+ @click.argument("prompt")
272
+ @click.option("--model", "model_name", required=True, help="Registered completion model name (from ai model list).")
273
+ @click.pass_context
274
+ def ai_complete(ctx: click.Context, prompt: str, model_name: str) -> None:
275
+ """Run text completion using the database AI_COMPLETE function."""
276
+ fmt: str = ctx.obj["format"]
277
+ dsn: str | None = ctx.obj["dsn"]
278
+
279
+ try:
280
+ conn = get_connection(dsn)
281
+ except Exception as exc:
282
+ log_operation("ai complete", ok=False, error_code="CONNECTION_ERROR")
283
+ output.error("CONNECTION_ERROR", str(exc), fmt=fmt)
284
+ return
285
+
286
+ timer = output.Timer()
287
+ try:
288
+ with timer, conn.cursor() as cur:
289
+ cur.execute(
290
+ "SELECT AI_COMPLETE(%s, %s) AS response",
291
+ (model_name, prompt),
292
+ )
293
+ row = cur.fetchone()
294
+ response_text = (row.get("response") or row.get("RESPONSE") or "") if row else ""
295
+
296
+ log_operation("ai complete", ok=True, time_ms=timer.elapsed_ms)
297
+ output.success(
298
+ {"model": model_name, "response": response_text},
299
+ time_ms=timer.elapsed_ms,
300
+ fmt=fmt,
301
+ )
302
+ except pymysql.err.Error as exc:
303
+ log_operation("ai complete", ok=False, error_code="AI_ERROR")
304
+ output.error("AI_ERROR", str(exc), fmt=fmt)
305
+ finally:
306
+ conn.close()
@@ -0,0 +1,188 @@
1
+ """seekdb collection command — manage vector collections."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ import click
8
+
9
+ from seekdb_cli import output
10
+ from seekdb_cli.logger import log_operation
11
+ from seekdb_cli.vecconnection import get_vec_client
12
+
13
+
14
+ @click.group("collection")
15
+ @click.pass_context
16
+ def collection_cmd(ctx: click.Context) -> None:
17
+ """Manage vector collections."""
18
+
19
+
20
+ @collection_cmd.command("list")
21
+ @click.pass_context
22
+ def list_collections(ctx: click.Context) -> None:
23
+ """List all collections in the current database."""
24
+ fmt: str = ctx.obj["format"]
25
+ dsn: str | None = ctx.obj["dsn"]
26
+
27
+ try:
28
+ client = get_vec_client(dsn)
29
+ except (ImportError, ValueError) as exc:
30
+ log_operation("collection list", ok=False, error_code="CONNECTION_ERROR")
31
+ output.error("CONNECTION_ERROR", str(exc), fmt=fmt)
32
+ return
33
+
34
+ timer = output.Timer()
35
+ try:
36
+ with timer:
37
+ collections = client.list_collections()
38
+ result = []
39
+ for col in collections:
40
+ info: dict[str, Any] = {"name": col.name}
41
+ try:
42
+ info["count"] = col.count()
43
+ except Exception:
44
+ info["count"] = None
45
+ result.append(info)
46
+
47
+ log_operation("collection list", ok=True, time_ms=timer.elapsed_ms)
48
+ output.success(result, time_ms=timer.elapsed_ms, fmt=fmt)
49
+ except Exception as exc:
50
+ log_operation("collection list", ok=False, error_code="COLLECTION_ERROR")
51
+ output.error("COLLECTION_ERROR", str(exc), fmt=fmt)
52
+
53
+
54
+ @collection_cmd.command("create")
55
+ @click.argument("name")
56
+ @click.option("--dimension", "-d", type=int, default=384, help="Vector dimension (default: 384).")
57
+ @click.option("--distance", type=click.Choice(["cosine", "l2", "ip"]), default="cosine", help="Distance metric.")
58
+ @click.pass_context
59
+ def create_collection(ctx: click.Context, name: str, dimension: int, distance: str) -> None:
60
+ """Create a new collection."""
61
+ fmt: str = ctx.obj["format"]
62
+ dsn: str | None = ctx.obj["dsn"]
63
+
64
+ try:
65
+ client = get_vec_client(dsn)
66
+ except (ImportError, ValueError) as exc:
67
+ log_operation("collection create", ok=False, error_code="CONNECTION_ERROR")
68
+ output.error("CONNECTION_ERROR", str(exc), fmt=fmt)
69
+ return
70
+
71
+ timer = output.Timer()
72
+ try:
73
+ from pyseekdb import HNSWConfiguration
74
+
75
+ with timer:
76
+ config = HNSWConfiguration(dimension=dimension, distance=distance)
77
+ client.create_collection(
78
+ name=name,
79
+ configuration=config,
80
+ embedding_function=None,
81
+ )
82
+
83
+ log_operation("collection create", ok=True, time_ms=timer.elapsed_ms)
84
+ output.success(
85
+ {"name": name, "dimension": dimension, "distance": distance},
86
+ time_ms=timer.elapsed_ms,
87
+ fmt=fmt,
88
+ )
89
+ except Exception as exc:
90
+ log_operation("collection create", ok=False, error_code="COLLECTION_ERROR")
91
+ output.error("COLLECTION_ERROR", str(exc), fmt=fmt)
92
+
93
+
94
+ @collection_cmd.command("delete")
95
+ @click.argument("name")
96
+ @click.pass_context
97
+ def delete_collection(ctx: click.Context, name: str) -> None:
98
+ """Delete a collection."""
99
+ fmt: str = ctx.obj["format"]
100
+ dsn: str | None = ctx.obj["dsn"]
101
+
102
+ try:
103
+ client = get_vec_client(dsn)
104
+ except (ImportError, ValueError) as exc:
105
+ log_operation("collection delete", ok=False, error_code="CONNECTION_ERROR")
106
+ output.error("CONNECTION_ERROR", str(exc), fmt=fmt)
107
+ return
108
+
109
+ timer = output.Timer()
110
+ try:
111
+ with timer:
112
+ if not client.has_collection(name):
113
+ log_operation("collection delete", ok=False, error_code="COLLECTION_NOT_FOUND")
114
+ output.error(
115
+ "COLLECTION_NOT_FOUND",
116
+ f"Collection '{name}' does not exist",
117
+ fmt=fmt,
118
+ extra={"collections": [c.name for c in client.list_collections()]},
119
+ )
120
+ return
121
+ client.delete_collection(name)
122
+
123
+ log_operation("collection delete", ok=True, time_ms=timer.elapsed_ms)
124
+ output.success({"deleted": name}, time_ms=timer.elapsed_ms, fmt=fmt)
125
+ except SystemExit:
126
+ raise
127
+ except Exception as exc:
128
+ log_operation("collection delete", ok=False, error_code="COLLECTION_ERROR")
129
+ output.error("COLLECTION_ERROR", str(exc), fmt=fmt)
130
+
131
+
132
+ @collection_cmd.command("info")
133
+ @click.argument("name")
134
+ @click.pass_context
135
+ def info_collection(ctx: click.Context, name: str) -> None:
136
+ """Show collection details (count, peek)."""
137
+ fmt: str = ctx.obj["format"]
138
+ dsn: str | None = ctx.obj["dsn"]
139
+
140
+ try:
141
+ client = get_vec_client(dsn)
142
+ except (ImportError, ValueError) as exc:
143
+ log_operation("collection info", ok=False, error_code="CONNECTION_ERROR")
144
+ output.error("CONNECTION_ERROR", str(exc), fmt=fmt)
145
+ return
146
+
147
+ timer = output.Timer()
148
+ try:
149
+ with timer:
150
+ if not client.has_collection(name):
151
+ log_operation("collection info", ok=False, error_code="COLLECTION_NOT_FOUND")
152
+ output.error(
153
+ "COLLECTION_NOT_FOUND",
154
+ f"Collection '{name}' does not exist",
155
+ fmt=fmt,
156
+ extra={"collections": [c.name for c in client.list_collections()]},
157
+ )
158
+ return
159
+
160
+ col = client.get_collection(name, embedding_function=None)
161
+ count = col.count()
162
+ preview = col.peek(limit=3)
163
+
164
+ data: dict[str, Any] = {
165
+ "name": name,
166
+ "count": count,
167
+ "preview": {
168
+ "ids": preview.get("ids", []),
169
+ "documents": preview.get("documents", []),
170
+ "metadatas": preview.get("metadatas", []),
171
+ },
172
+ }
173
+ # Dimension and distance come from list_collections() metadata (pyseekdb API)
174
+ for coll in client.list_collections():
175
+ if getattr(coll, "name", None) == name:
176
+ if getattr(coll, "dimension", None) is not None:
177
+ data["dimension"] = coll.dimension
178
+ if getattr(coll, "distance", None) is not None:
179
+ data["distance"] = coll.distance
180
+ break
181
+
182
+ log_operation("collection info", ok=True, time_ms=timer.elapsed_ms)
183
+ output.success(data, time_ms=timer.elapsed_ms, fmt=fmt)
184
+ except SystemExit:
185
+ raise
186
+ except Exception as exc:
187
+ log_operation("collection info", ok=False, error_code="COLLECTION_ERROR")
188
+ output.error("COLLECTION_ERROR", str(exc), fmt=fmt)