magickmind 0.1.1__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 (43) hide show
  1. magick_mind/__init__.py +39 -0
  2. magick_mind/auth/__init__.py +9 -0
  3. magick_mind/auth/base.py +46 -0
  4. magick_mind/auth/email_password.py +268 -0
  5. magick_mind/client.py +188 -0
  6. magick_mind/config.py +28 -0
  7. magick_mind/exceptions.py +107 -0
  8. magick_mind/http/__init__.py +5 -0
  9. magick_mind/http/client.py +313 -0
  10. magick_mind/models/__init__.py +17 -0
  11. magick_mind/models/auth.py +30 -0
  12. magick_mind/models/common.py +32 -0
  13. magick_mind/models/errors.py +73 -0
  14. magick_mind/models/v1/__init__.py +83 -0
  15. magick_mind/models/v1/api_keys.py +115 -0
  16. magick_mind/models/v1/artifact.py +151 -0
  17. magick_mind/models/v1/chat.py +104 -0
  18. magick_mind/models/v1/corpus.py +82 -0
  19. magick_mind/models/v1/end_user.py +75 -0
  20. magick_mind/models/v1/history.py +94 -0
  21. magick_mind/models/v1/mindspace.py +130 -0
  22. magick_mind/models/v1/model.py +25 -0
  23. magick_mind/models/v1/project.py +73 -0
  24. magick_mind/realtime/__init__.py +5 -0
  25. magick_mind/realtime/client.py +202 -0
  26. magick_mind/realtime/handler.py +122 -0
  27. magick_mind/resources/README.md +201 -0
  28. magick_mind/resources/__init__.py +42 -0
  29. magick_mind/resources/base.py +31 -0
  30. magick_mind/resources/v1/__init__.py +19 -0
  31. magick_mind/resources/v1/api_keys.py +181 -0
  32. magick_mind/resources/v1/artifact.py +287 -0
  33. magick_mind/resources/v1/chat.py +120 -0
  34. magick_mind/resources/v1/corpus.py +156 -0
  35. magick_mind/resources/v1/end_user.py +181 -0
  36. magick_mind/resources/v1/history.py +88 -0
  37. magick_mind/resources/v1/mindspace.py +331 -0
  38. magick_mind/resources/v1/model.py +19 -0
  39. magick_mind/resources/v1/project.py +155 -0
  40. magick_mind/routes.py +76 -0
  41. magickmind-0.1.1.dist-info/METADATA +593 -0
  42. magickmind-0.1.1.dist-info/RECORD +43 -0
  43. magickmind-0.1.1.dist-info/WHEEL +4 -0
@@ -0,0 +1,181 @@
1
+ """
2
+ API Keys resource for Magick Mind SDK v1 API.
3
+
4
+ Provides methods for managing API keys for authenticated requests.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import TYPE_CHECKING, Optional
10
+
11
+ from magick_mind.models.v1.api_keys import (
12
+ CreateApiKeyRequest,
13
+ CreateApiKeyResponse,
14
+ DeleteApiKeyResponse,
15
+ ListApiKeysResponse,
16
+ UpdateApiKeyRequest,
17
+ UpdateApiKeyResponse,
18
+ )
19
+ from magick_mind.routes import Routes
20
+
21
+ if TYPE_CHECKING:
22
+ import httpx
23
+
24
+
25
+ class ApiKeysResourceV1:
26
+ """Resource client for API key operations."""
27
+
28
+ def __init__(self, http_client: httpx.Client):
29
+ """
30
+ Initialize the API keys resource.
31
+
32
+ Args:
33
+ http_client: Authenticated httpx client
34
+ """
35
+ self.http = http_client
36
+
37
+ def create(
38
+ self,
39
+ user_id: str,
40
+ project_id: str,
41
+ models: list[str],
42
+ key_alias: str,
43
+ duration: Optional[str] = None,
44
+ team_id: Optional[str] = None,
45
+ max_budget: Optional[float] = None,
46
+ ) -> CreateApiKeyResponse:
47
+ """
48
+ Create a new API key.
49
+
50
+ Args:
51
+ user_id: User ID that owns this key
52
+ project_id: Project ID to associate with
53
+ models: List of allowed model names
54
+ key_alias: Human-readable key name
55
+ duration: Optional validity duration (e.g., '30d', '1y')
56
+ team_id: Optional team ID
57
+ max_budget: Optional spending limit
58
+
59
+ Returns:
60
+ CreateApiKeyResponse with the new API key
61
+
62
+ Raises:
63
+ httpx.HTTPStatusError: If the request fails
64
+
65
+ Example:
66
+ >>> response = client.v1.api_keys.create(
67
+ ... user_id="user-123",
68
+ ... project_id="proj-456",
69
+ ... models=["gpt-4", "gpt-3.5-turbo"],
70
+ ... key_alias="Production Key",
71
+ ... duration="90d",
72
+ ... max_budget=100.0
73
+ ... )
74
+ >>> print(f"API Key: {response.key.key}")
75
+ """
76
+ payload = CreateApiKeyRequest(
77
+ user_id=user_id,
78
+ project_id=project_id,
79
+ models=models,
80
+ key_alias=key_alias,
81
+ duration=duration,
82
+ team_id=team_id,
83
+ max_budget=max_budget,
84
+ )
85
+
86
+ resp = self.http.post(Routes.KEYS, json=payload.model_dump(exclude_none=True))
87
+ resp.raise_for_status()
88
+
89
+ return CreateApiKeyResponse(**resp.json())
90
+
91
+ def list(self, user_id: str) -> ListApiKeysResponse:
92
+ """
93
+ List all API keys for a user.
94
+
95
+ Args:
96
+ user_id: User ID to list keys for
97
+
98
+ Returns:
99
+ ListApiKeysResponse with list of API keys
100
+
101
+ Raises:
102
+ httpx.HTTPStatusError: If the request fails
103
+
104
+ Example:
105
+ >>> response = client.v1.api_keys.list(user_id="user-123")
106
+ >>> for key in response.keys:
107
+ ... print(f"{key.key_alias}: {key.key_id}")
108
+ """
109
+ resp = self.http.get(Routes.KEYS, params={"user_id": user_id})
110
+ resp.raise_for_status()
111
+
112
+ return ListApiKeysResponse(**resp.json())
113
+
114
+ def update(
115
+ self,
116
+ key: str,
117
+ models: list[str],
118
+ key_alias: str,
119
+ duration: Optional[str] = None,
120
+ max_budget: Optional[float] = None,
121
+ ) -> UpdateApiKeyResponse:
122
+ """
123
+ Update an existing API key.
124
+
125
+ Args:
126
+ key: The API key to update
127
+ models: Updated list of allowed models
128
+ key_alias: Updated key alias/name
129
+ duration: Optional updated validity duration
130
+ max_budget: Optional updated spending limit
131
+
132
+ Returns:
133
+ UpdateApiKeyResponse with updated key details
134
+
135
+ Raises:
136
+ httpx.HTTPStatusError: If the request fails
137
+
138
+ Example:
139
+ >>> response = client.v1.api_keys.update(
140
+ ... key="sk-...",
141
+ ... models=["gpt-4", "claude-3"],
142
+ ... key_alias="Updated Production Key",
143
+ ... max_budget=200.0
144
+ ... )
145
+ """
146
+ payload = UpdateApiKeyRequest(
147
+ key=key,
148
+ models=models,
149
+ key_alias=key_alias,
150
+ duration=duration,
151
+ max_budget=max_budget,
152
+ )
153
+
154
+ resp = self.http.put(Routes.KEYS, json=payload.model_dump(exclude_none=True))
155
+ resp.raise_for_status()
156
+
157
+ return UpdateApiKeyResponse(**resp.json())
158
+
159
+ def delete(self, key_id: str) -> DeleteApiKeyResponse:
160
+ """
161
+ Delete an API key.
162
+
163
+ Args:
164
+ key_id: The key ID to delete
165
+
166
+ Returns:
167
+ DeleteApiKeyResponse with confirmation
168
+
169
+ Raises:
170
+ httpx.HTTPStatusError: If the request fails
171
+
172
+ Example:
173
+ >>> response = client.v1.api_keys.delete(key_id="key-abc-123")
174
+ >>> print(response.message)
175
+ """
176
+ payload = {"key_id": key_id}
177
+
178
+ resp = self.http.delete(Routes.KEYS, json=payload)
179
+ resp.raise_for_status()
180
+
181
+ return DeleteApiKeyResponse(**resp.json())
@@ -0,0 +1,287 @@
1
+ """
2
+ Artifact resource for Magick Mind SDK v1 API.
3
+
4
+ Provides methods for file upload via presigned S3 URLs and artifact management.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import TYPE_CHECKING, Optional
10
+
11
+ import httpx
12
+
13
+ from magick_mind.models.v1.artifact import (
14
+ Artifact,
15
+ FinalizeArtifactRequest,
16
+ FinalizeArtifactResponse,
17
+ GetArtifactResponse,
18
+ ListArtifactsResponse,
19
+ PresignArtifactRequest,
20
+ PresignArtifactResponse,
21
+ )
22
+ from magick_mind.resources.base import BaseResource
23
+ from magick_mind.routes import Routes
24
+
25
+ if TYPE_CHECKING:
26
+ from httpx import Response
27
+
28
+
29
+ class ArtifactResourceV1(BaseResource):
30
+ """
31
+ Artifact resource for managing file uploads and artifacts.
32
+
33
+ Implements presigned S3 upload flow:
34
+ 1. Call presign_upload() to get a presigned URL
35
+ 2. Upload file directly to S3 using the presigned URL
36
+ 3. Webhook or finalize() confirms completion
37
+ 4. Use get() or list() to retrieve artifact metadata
38
+ """
39
+
40
+ def presign_upload(
41
+ self,
42
+ file_name: str,
43
+ content_type: str,
44
+ size_bytes: int,
45
+ end_user_id: Optional[str] = None,
46
+ corpus_id: Optional[str] = None,
47
+ ) -> PresignArtifactResponse:
48
+ """
49
+ Get a presigned S3 URL for uploading a file.
50
+
51
+ Args:
52
+ file_name: Name of the file to upload (required)
53
+ content_type: MIME type of the file (e.g., 'application/pdf', 'image/png')
54
+ size_bytes: File size in bytes (must be > 0)
55
+ end_user_id: Optional end user identifier
56
+ corpus_id: Optional corpus ID to associate artifact with
57
+
58
+ Returns:
59
+ PresignArtifactResponse containing upload_url and metadata
60
+
61
+ Example:
62
+ # Get presigned URL
63
+ response = client.v1.artifact.presign_upload(
64
+ file_name="document.pdf",
65
+ content_type="application/pdf",
66
+ size_bytes=1024000,
67
+ corpus_id="corpus-123"
68
+ )
69
+
70
+ # Upload file to S3 using httpx
71
+ import httpx
72
+ with open("document.pdf", "rb") as f:
73
+ httpx.put(
74
+ response.upload_url,
75
+ content=f,
76
+ headers=response.required_headers
77
+ )
78
+
79
+ print(f"Artifact ID: {response.id}")
80
+ """
81
+ request = PresignArtifactRequest(
82
+ file_name=file_name,
83
+ content_type=content_type,
84
+ size_bytes=size_bytes,
85
+ end_user_id=end_user_id,
86
+ corpus_id=corpus_id,
87
+ )
88
+
89
+ # Use generic presign endpoint
90
+ response = self._http.post(
91
+ Routes.ARTIFACTS_PRESIGN, json=request.model_dump(exclude_none=True)
92
+ )
93
+ return PresignArtifactResponse(**response.json())
94
+
95
+ def upload_file(
96
+ self,
97
+ file_path: str,
98
+ content_type: str,
99
+ end_user_id: Optional[str] = None,
100
+ corpus_id: Optional[str] = None,
101
+ ) -> tuple[PresignArtifactResponse, "Response"]:
102
+ """
103
+ Convenience method to presign and upload a file in one call.
104
+
105
+ This is a high-level wrapper that:
106
+ 1. Gets the file size
107
+ 2. Obtains presigned URL
108
+ 3. Uploads the file to S3
109
+
110
+ Args:
111
+ file_path: Path to the file to upload
112
+ content_type: MIME type of the file
113
+ end_user_id: Optional end user identifier
114
+ corpus_id: Optional corpus ID to associate artifact with
115
+
116
+ Returns:
117
+ Tuple of (PresignArtifactResponse, S3 upload response)
118
+
119
+ Example:
120
+ presign_resp, upload_resp = client.v1.artifact.upload_file(
121
+ file_path="./document.pdf",
122
+ content_type="application/pdf",
123
+ corpus_id="corpus-123"
124
+ )
125
+
126
+ if upload_resp.status_code == 200:
127
+ print(f"Upload successful! Artifact ID: {presign_resp.id}")
128
+ """
129
+ import os
130
+
131
+ # Get file size
132
+ size_bytes = os.path.getsize(file_path)
133
+ file_name = os.path.basename(file_path)
134
+
135
+ # Get presigned URL
136
+ presign_response = self.presign_upload(
137
+ file_name=file_name,
138
+ content_type=content_type,
139
+ size_bytes=size_bytes,
140
+ end_user_id=end_user_id,
141
+ corpus_id=corpus_id,
142
+ )
143
+
144
+ # Upload to S3 using httpx
145
+ with open(file_path, "rb") as f:
146
+ upload_response = httpx.put(
147
+ presign_response.upload_url,
148
+ content=f,
149
+ headers=presign_response.required_headers,
150
+ )
151
+
152
+ upload_response.raise_for_status()
153
+ return presign_response, upload_response
154
+
155
+ def get(self, artifact_id: str) -> Artifact:
156
+ """
157
+ Get artifact metadata by ID.
158
+
159
+ Args:
160
+ artifact_id: The artifact ID to retrieve
161
+
162
+ Returns:
163
+ Artifact object with metadata
164
+
165
+ Example:
166
+ artifact = client.v1.artifact.get(artifact_id="art-123")
167
+ print(f"Status: {artifact.status}")
168
+ print(f"S3 URL: {artifact.s3_url}")
169
+ """
170
+ response = self._http.get(Routes.artifact(artifact_id))
171
+ get_response = GetArtifactResponse(**response.json())
172
+ return get_response.artifact
173
+
174
+ def list(
175
+ self,
176
+ corpus_id: Optional[str] = None,
177
+ end_user_id: Optional[str] = None,
178
+ status: Optional[str] = None,
179
+ ) -> list[Artifact]:
180
+ """
181
+ List/query artifacts with optional filters.
182
+
183
+ Args:
184
+ corpus_id: Filter by corpus ID (optional)
185
+ end_user_id: Filter by end user ID (optional)
186
+ status: Filter by status (uploaded, processing, ready, failed)
187
+
188
+ Returns:
189
+ List of Artifact objects
190
+
191
+ Example:
192
+ # Get all artifacts for a corpus
193
+ artifacts = client.v1.artifact.list(corpus_id="corpus-123")
194
+ for artifact in artifacts:
195
+ print(f"- {artifact.id}: {artifact.status}")
196
+
197
+ # Get ready artifacts
198
+ ready = client.v1.artifact.list(status="ready")
199
+ """
200
+ params = {}
201
+ if corpus_id is not None:
202
+ params["corpus_id"] = corpus_id
203
+ if end_user_id is not None:
204
+ params["end_user_id"] = end_user_id
205
+ if status is not None:
206
+ params["status"] = status
207
+
208
+ response = self._http.get(Routes.ARTIFACTS, params=params)
209
+ list_response = ListArtifactsResponse(**response.json())
210
+ return list_response.artifacts
211
+
212
+ def delete(self, artifact_id: str) -> None:
213
+ """
214
+ Delete an artifact.
215
+
216
+ Args:
217
+ artifact_id: The artifact ID to delete
218
+
219
+ Example:
220
+ client.v1.artifact.delete(artifact_id="art-123")
221
+ print("Artifact deleted successfully")
222
+ """
223
+ self._http.delete(Routes.artifact(artifact_id))
224
+
225
+ def finalize(
226
+ self,
227
+ artifact_id: str,
228
+ bucket: str,
229
+ key: str,
230
+ version_id: Optional[str] = None,
231
+ size_bytes: Optional[int] = None,
232
+ content_type: Optional[str] = None,
233
+ etag: Optional[str] = None,
234
+ checksum_sha256: Optional[str] = None,
235
+ corpus_id: Optional[str] = None,
236
+ ) -> FinalizeArtifactResponse:
237
+ """
238
+ Client-driven finalize (fallback when webhook is unavailable).
239
+
240
+ This is typically used in local development or when the S3 Lambda
241
+ webhook path is not available. In production, webhooks handle
242
+ finalization automatically.
243
+
244
+ Args:
245
+ artifact_id: Artifact ID from presign response
246
+ bucket: S3 bucket name
247
+ key: S3 object key
248
+ version_id: S3 version ID (optional)
249
+ size_bytes: Actual uploaded size (optional)
250
+ content_type: Content type (optional)
251
+ etag: S3 ETag (optional)
252
+ checksum_sha256: SHA256 checksum (optional)
253
+ corpus_id: Corpus ID to finalize under (optional)
254
+
255
+ Returns:
256
+ FinalizeArtifactResponse
257
+
258
+ Example:
259
+ # After uploading to S3
260
+ response = client.v1.artifact.finalize(
261
+ artifact_id=presign_resp.id,
262
+ bucket=presign_resp.bucket,
263
+ key=presign_resp.key,
264
+ corpus_id="corpus-123"
265
+ )
266
+ print(f"Finalize status: {response.success}")
267
+ """
268
+ request = FinalizeArtifactRequest(
269
+ artifact_id=artifact_id,
270
+ bucket=bucket,
271
+ key=key,
272
+ version_id=version_id,
273
+ size_bytes=size_bytes,
274
+ content_type=content_type,
275
+ etag=etag,
276
+ checksum_sha256=checksum_sha256,
277
+ )
278
+
279
+ # Route to corpus-scoped finalize if corpus_id provided
280
+ if corpus_id:
281
+ endpoint = Routes.corpus_artifacts_finalize(corpus_id)
282
+ else:
283
+ # Generic finalize endpoint (if available)
284
+ endpoint = Routes.ARTIFACTS_FINALIZE
285
+
286
+ response = self._http.post(endpoint, json=request.model_dump(exclude_none=True))
287
+ return FinalizeArtifactResponse(**response.json())
@@ -0,0 +1,120 @@
1
+ """V1 chat resource implementation."""
2
+
3
+ from typing import TYPE_CHECKING, Optional
4
+
5
+ from magick_mind.models.v1.chat import (
6
+ ChatSendRequest,
7
+ ChatSendResponse,
8
+ ConfigSchema,
9
+ )
10
+ from magick_mind.resources.base import BaseResource
11
+ from magick_mind.routes import Routes
12
+
13
+ if TYPE_CHECKING:
14
+ from magick_mind.http import HTTPClient
15
+
16
+
17
+ class ChatResourceV1(BaseResource):
18
+ """
19
+ Chat resource client for V1 API.
20
+
21
+ Provides typed interface for sending chat messages to mindspaces.
22
+
23
+ Example:
24
+ response = client.v1.chat.send(
25
+ api_key="sk-...",
26
+ mindspace_id="mind-123",
27
+ message="Hello!",
28
+ enduser_id="user-456",
29
+ config=ConfigSchema(
30
+ fast_model_id="gpt-4",
31
+ smart_model_ids=["gpt-4"],
32
+ compute_power=50,
33
+ ),
34
+ )
35
+ print(response.content.content) # AI response text
36
+ """
37
+
38
+ def send(
39
+ self,
40
+ api_key: str,
41
+ mindspace_id: str,
42
+ message: str,
43
+ enduser_id: str,
44
+ config: ConfigSchema,
45
+ reply_to_message_id: Optional[str] = None,
46
+ additional_context: Optional[str] = None,
47
+ artifact_ids: Optional[list[str]] = None,
48
+ ) -> ChatSendResponse:
49
+ """
50
+ Send a chat message to a mindspace and get AI response.
51
+
52
+ Args:
53
+ api_key: API key for LLM access
54
+ mindspace_id: Mindspace/chat conversation ID
55
+ message: User message text to send
56
+ enduser_id: End-user identifier
57
+ config: Model configuration (fast_model_id, smart_model_ids, compute_power)
58
+ reply_to_message_id: Optional ID of message being replied to
59
+ additional_context: Optional additional context for the message
60
+ artifact_ids: Optional list of artifact IDs to attach to message
61
+
62
+ Returns:
63
+ ChatSendResponse with AI-generated response
64
+
65
+ Raises:
66
+ ValidationError: If message is empty, mindspace_id invalid, or required fields missing
67
+ ProblemDetailsException: If mindspace not found (404), permission denied (403),
68
+ or server error (500+). Always includes request_id for support.
69
+ RateLimitError: If API rate limit exceeded (429)
70
+ AuthenticationError: If JWT token is invalid or expired (auto-refreshed transparently)
71
+
72
+ Example:
73
+ # Basic chat with config
74
+ response = chat.send(
75
+ api_key="sk-test",
76
+ mindspace_id="mind-123",
77
+ message="What's the weather?",
78
+ enduser_id="user-456",
79
+ config=ConfigSchema(
80
+ fast_model_id="gpt-4",
81
+ smart_model_ids=["gpt-4"],
82
+ ),
83
+ )
84
+
85
+ # Chat with attached artifacts
86
+ response = chat.send(
87
+ api_key="sk-test",
88
+ mindspace_id="mind-123",
89
+ message="Analyze these documents",
90
+ enduser_id="user-456",
91
+ config=ConfigSchema(
92
+ fast_model_id="gpt-4",
93
+ smart_model_ids=["gpt-4"],
94
+ compute_power=80,
95
+ ),
96
+ artifact_ids=["art-123", "art-456"],
97
+ )
98
+
99
+ print(f"AI: {response.content.content}")
100
+ print(f"Message ID: {response.content.message_id}")
101
+ """
102
+ # Build and validate request
103
+ request = ChatSendRequest(
104
+ api_key=api_key,
105
+ mindspace_id=mindspace_id,
106
+ message=message,
107
+ enduser_id=enduser_id,
108
+ config=config,
109
+ reply_to_message_id=reply_to_message_id,
110
+ additional_context=additional_context,
111
+ artifact_ids=artifact_ids,
112
+ )
113
+
114
+ # Make API call
115
+ response = self._http.post(
116
+ Routes.CHAT, json=request.model_dump(exclude_none=True)
117
+ )
118
+
119
+ # Parse and validate response
120
+ return ChatSendResponse.model_validate(response)