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.
- magick_mind/__init__.py +39 -0
- magick_mind/auth/__init__.py +9 -0
- magick_mind/auth/base.py +46 -0
- magick_mind/auth/email_password.py +268 -0
- magick_mind/client.py +188 -0
- magick_mind/config.py +28 -0
- magick_mind/exceptions.py +107 -0
- magick_mind/http/__init__.py +5 -0
- magick_mind/http/client.py +313 -0
- magick_mind/models/__init__.py +17 -0
- magick_mind/models/auth.py +30 -0
- magick_mind/models/common.py +32 -0
- magick_mind/models/errors.py +73 -0
- magick_mind/models/v1/__init__.py +83 -0
- magick_mind/models/v1/api_keys.py +115 -0
- magick_mind/models/v1/artifact.py +151 -0
- magick_mind/models/v1/chat.py +104 -0
- magick_mind/models/v1/corpus.py +82 -0
- magick_mind/models/v1/end_user.py +75 -0
- magick_mind/models/v1/history.py +94 -0
- magick_mind/models/v1/mindspace.py +130 -0
- magick_mind/models/v1/model.py +25 -0
- magick_mind/models/v1/project.py +73 -0
- magick_mind/realtime/__init__.py +5 -0
- magick_mind/realtime/client.py +202 -0
- magick_mind/realtime/handler.py +122 -0
- magick_mind/resources/README.md +201 -0
- magick_mind/resources/__init__.py +42 -0
- magick_mind/resources/base.py +31 -0
- magick_mind/resources/v1/__init__.py +19 -0
- magick_mind/resources/v1/api_keys.py +181 -0
- magick_mind/resources/v1/artifact.py +287 -0
- magick_mind/resources/v1/chat.py +120 -0
- magick_mind/resources/v1/corpus.py +156 -0
- magick_mind/resources/v1/end_user.py +181 -0
- magick_mind/resources/v1/history.py +88 -0
- magick_mind/resources/v1/mindspace.py +331 -0
- magick_mind/resources/v1/model.py +19 -0
- magick_mind/resources/v1/project.py +155 -0
- magick_mind/routes.py +76 -0
- magickmind-0.1.1.dist-info/METADATA +593 -0
- magickmind-0.1.1.dist-info/RECORD +43 -0
- magickmind-0.1.1.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Project models for Magick Mind SDK v1 API.
|
|
3
|
+
|
|
4
|
+
Mirrors Bifrost's /v1/projects endpoint request/response schemas.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Optional, List
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
10
|
+
|
|
11
|
+
from magick_mind.models.v1.end_user import PageInfo
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Project(BaseModel):
|
|
15
|
+
"""
|
|
16
|
+
Project schema from Bifrost.
|
|
17
|
+
|
|
18
|
+
Represents an agentic SaaS project with associated corpus IDs.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
22
|
+
|
|
23
|
+
id: str = Field(..., description="Project ID")
|
|
24
|
+
name: str = Field(..., description="Project name")
|
|
25
|
+
description: Optional[str] = Field(None, description="Project description")
|
|
26
|
+
corpus_ids: Optional[list[str]] = Field(
|
|
27
|
+
None,
|
|
28
|
+
description="List of corpus IDs associated with this project",
|
|
29
|
+
)
|
|
30
|
+
created_by: str = Field(..., description="User ID of creator")
|
|
31
|
+
created_at: str = Field(..., description="Creation timestamp (ISO8601)")
|
|
32
|
+
updated_at: str = Field(..., description="Last update timestamp (ISO8601)")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class CreateProjectRequest(BaseModel):
|
|
36
|
+
"""
|
|
37
|
+
Request schema for creating a new project.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
name: str = Field(..., description="Project name (required)")
|
|
41
|
+
description: str = Field(
|
|
42
|
+
default="", description="Project description (optional, max 256 chars)"
|
|
43
|
+
)
|
|
44
|
+
corpus_ids: list[str] = Field(
|
|
45
|
+
default_factory=list, description="List of corpus IDs to associate with project"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class GetProjectListResponse(BaseModel):
|
|
50
|
+
"""
|
|
51
|
+
Response schema for listing projects.
|
|
52
|
+
|
|
53
|
+
Matches Bifrost's {data: list[Project], paging: PageInfo} structure.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
data: list[Project] = Field(..., description="List of projects")
|
|
57
|
+
paging: PageInfo = Field(..., description="Pagination information")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class UpdateProjectRequest(BaseModel):
|
|
61
|
+
"""
|
|
62
|
+
Request schema for updating a project.
|
|
63
|
+
|
|
64
|
+
Both name and corpus_ids are REQUIRED (matching Bifrost API).
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
name: str = Field(..., description="Project name (required)")
|
|
68
|
+
description: Optional[str] = Field(
|
|
69
|
+
None, description="Project description (optional, max 256 chars)"
|
|
70
|
+
)
|
|
71
|
+
corpus_ids: list[str] = Field(
|
|
72
|
+
..., description="List of corpus IDs to associate with project (required)"
|
|
73
|
+
)
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""Realtime client implementation using centrifuge-python."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import base64
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
from typing import Optional, List
|
|
8
|
+
|
|
9
|
+
from centrifuge import (
|
|
10
|
+
Client,
|
|
11
|
+
ClientEventHandler,
|
|
12
|
+
PublicationContext,
|
|
13
|
+
SubscriptionEventHandler,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
from ..auth.base import AuthProvider
|
|
17
|
+
from ..exceptions import MagickMindError
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _extract_jwt_sub(token: str) -> Optional[str]:
|
|
24
|
+
"""
|
|
25
|
+
Decode JWT without verification to extract 'sub'.
|
|
26
|
+
Returns None if parsing fails.
|
|
27
|
+
"""
|
|
28
|
+
try:
|
|
29
|
+
parts = token.split(".")
|
|
30
|
+
if len(parts) < 2:
|
|
31
|
+
return None
|
|
32
|
+
payload_b64 = parts[1]
|
|
33
|
+
payload_b64 += "=" * ((4 - len(payload_b64) % 4) % 4)
|
|
34
|
+
payload = json.loads(base64.urlsafe_b64decode(payload_b64).decode("utf-8"))
|
|
35
|
+
sub = payload.get("sub")
|
|
36
|
+
return sub if isinstance(sub, str) else None
|
|
37
|
+
except Exception:
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class _DelegatingSubscriptionHandler(SubscriptionEventHandler):
|
|
42
|
+
"""Routes client-side subscription publications to ClientEventHandler.on_publication."""
|
|
43
|
+
|
|
44
|
+
def __init__(self, client_handler: ClientEventHandler, channel: str):
|
|
45
|
+
self._client_handler = client_handler
|
|
46
|
+
self._channel = channel
|
|
47
|
+
|
|
48
|
+
async def on_publication(self, ctx: PublicationContext) -> None:
|
|
49
|
+
"""Route client-side publication to the ClientEventHandler."""
|
|
50
|
+
logger.debug(f"Publication on {self._channel}: {ctx.pub.data}")
|
|
51
|
+
|
|
52
|
+
# Wrap in adapter for ClientEventHandler
|
|
53
|
+
server_ctx = _PublicationAdapter(ctx, self._channel)
|
|
54
|
+
try:
|
|
55
|
+
await self._client_handler.on_publication(server_ctx)
|
|
56
|
+
except Exception:
|
|
57
|
+
logger.exception(f"Error in on_publication handler for {self._channel}")
|
|
58
|
+
|
|
59
|
+
async def on_subscribed(self, ctx) -> None:
|
|
60
|
+
logger.info(f"✅ Subscribed to channel: {self._channel}")
|
|
61
|
+
|
|
62
|
+
async def on_unsubscribed(self, ctx) -> None:
|
|
63
|
+
logger.info(f"Unsubscribed from channel: {self._channel}")
|
|
64
|
+
|
|
65
|
+
async def on_error(self, ctx) -> None:
|
|
66
|
+
logger.error(f"Subscription error on {self._channel}: {ctx}")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class _PublicationAdapter:
|
|
70
|
+
"""Adapts PublicationContext to look like ServerPublicationContext."""
|
|
71
|
+
|
|
72
|
+
def __init__(self, client_ctx: PublicationContext, channel: str):
|
|
73
|
+
self.pub = client_ctx.pub
|
|
74
|
+
self.channel = channel
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class RealtimeClient:
|
|
78
|
+
"""
|
|
79
|
+
Async client for real-time features using WebSockets.
|
|
80
|
+
Uses pure client-side subscriptions for reliability.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
def __init__(self, auth: AuthProvider, ws_url: str):
|
|
84
|
+
self.auth = auth
|
|
85
|
+
self.ws_url = ws_url
|
|
86
|
+
self._client: Optional[Client] = None
|
|
87
|
+
self._events: Optional[ClientEventHandler] = None
|
|
88
|
+
|
|
89
|
+
async def _get_token(self) -> str:
|
|
90
|
+
"""Get token wrapper for centrifuge client."""
|
|
91
|
+
try:
|
|
92
|
+
return await self.auth.get_token_async()
|
|
93
|
+
except Exception:
|
|
94
|
+
raise
|
|
95
|
+
|
|
96
|
+
async def connect(self, events: Optional[ClientEventHandler] = None) -> None:
|
|
97
|
+
"""Connect to the realtime service."""
|
|
98
|
+
if self._client:
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
self._events = events or ClientEventHandler()
|
|
102
|
+
|
|
103
|
+
self._client = Client(
|
|
104
|
+
self.ws_url,
|
|
105
|
+
events=self._events,
|
|
106
|
+
get_token=self._get_token,
|
|
107
|
+
use_protobuf=False,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
await self._client.connect()
|
|
111
|
+
await self._client.ready()
|
|
112
|
+
|
|
113
|
+
async def disconnect(self) -> None:
|
|
114
|
+
"""Disconnect from the realtime service."""
|
|
115
|
+
if self._client:
|
|
116
|
+
await self._client.disconnect()
|
|
117
|
+
self._client = None
|
|
118
|
+
|
|
119
|
+
async def subscribe(self, target_user_id: str) -> None:
|
|
120
|
+
"""
|
|
121
|
+
Subscribe to a user's channel using client-side subscription.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
target_user_id: ID of the user to subscribe to
|
|
125
|
+
"""
|
|
126
|
+
if not self._client:
|
|
127
|
+
raise MagickMindError("Realtime client not connected")
|
|
128
|
+
|
|
129
|
+
# Build channel name
|
|
130
|
+
token = await self._get_token()
|
|
131
|
+
service_user_id = _extract_jwt_sub(token)
|
|
132
|
+
if not service_user_id:
|
|
133
|
+
raise MagickMindError("Failed to extract service_user_id from JWT")
|
|
134
|
+
|
|
135
|
+
channel = f"personal:{target_user_id}#{service_user_id}"
|
|
136
|
+
logger.debug(f"Subscribing to channel: {channel}")
|
|
137
|
+
|
|
138
|
+
# Create client-side subscription with handler
|
|
139
|
+
await self._ensure_subscription(channel)
|
|
140
|
+
|
|
141
|
+
async def _ensure_subscription(self, channel: str) -> None:
|
|
142
|
+
"""Ensure client-side subscription exists with proper event handler."""
|
|
143
|
+
if not self._client or not self._events:
|
|
144
|
+
return
|
|
145
|
+
|
|
146
|
+
sub_events = _DelegatingSubscriptionHandler(self._events, channel)
|
|
147
|
+
|
|
148
|
+
try:
|
|
149
|
+
existing_sub = self._client.get_subscription(channel)
|
|
150
|
+
if existing_sub:
|
|
151
|
+
state = getattr(existing_sub, "state", None)
|
|
152
|
+
state_name = getattr(state, "name", "")
|
|
153
|
+
if state_name == "UNSUBSCRIBED":
|
|
154
|
+
await existing_sub.subscribe()
|
|
155
|
+
logger.info(f"Resubscribed to {channel}")
|
|
156
|
+
else:
|
|
157
|
+
sub = self._client.new_subscription(channel, events=sub_events)
|
|
158
|
+
await sub.subscribe()
|
|
159
|
+
logger.info(f"Subscribed to {channel}")
|
|
160
|
+
except Exception as e:
|
|
161
|
+
logger.error(f"Subscription failed for {channel}: {e}")
|
|
162
|
+
raise MagickMindError(f"Subscribe failed: {e}")
|
|
163
|
+
|
|
164
|
+
async def subscribe_many(self, target_user_ids: List[str]) -> None:
|
|
165
|
+
"""Subscribe to multiple users concurrently."""
|
|
166
|
+
if not target_user_ids:
|
|
167
|
+
return
|
|
168
|
+
|
|
169
|
+
tasks = [self.subscribe(uid) for uid in target_user_ids]
|
|
170
|
+
results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
171
|
+
|
|
172
|
+
errors = [r for r in results if isinstance(r, Exception)]
|
|
173
|
+
if errors:
|
|
174
|
+
raise errors[0]
|
|
175
|
+
|
|
176
|
+
async def unsubscribe(self, target_user_id: str) -> None:
|
|
177
|
+
"""Unsubscribe from a user's channel."""
|
|
178
|
+
if not self._client:
|
|
179
|
+
raise MagickMindError("Realtime client not connected")
|
|
180
|
+
|
|
181
|
+
token = await self._get_token()
|
|
182
|
+
service_user_id = _extract_jwt_sub(token)
|
|
183
|
+
if not service_user_id:
|
|
184
|
+
return
|
|
185
|
+
|
|
186
|
+
channel = f"personal:{target_user_id}#{service_user_id}"
|
|
187
|
+
sub = self._client.get_subscription(channel)
|
|
188
|
+
if sub:
|
|
189
|
+
await sub.unsubscribe()
|
|
190
|
+
|
|
191
|
+
async def unsubscribe_many(self, target_user_ids: List[str]) -> None:
|
|
192
|
+
"""Unsubscribe from multiple users concurrently."""
|
|
193
|
+
if not target_user_ids:
|
|
194
|
+
return
|
|
195
|
+
|
|
196
|
+
tasks = [self.unsubscribe(uid) for uid in target_user_ids]
|
|
197
|
+
await asyncio.gather(*tasks, return_exceptions=True)
|
|
198
|
+
|
|
199
|
+
@property
|
|
200
|
+
def client(self) -> Optional[Client]:
|
|
201
|
+
"""Get underlying centrifuge client."""
|
|
202
|
+
return self._client
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""
|
|
2
|
+
High-level event handler for Magick Mind Realtime Client.
|
|
3
|
+
Abstracts away Centrifugo channel parsing details.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
from typing import Any, Optional
|
|
8
|
+
|
|
9
|
+
from centrifuge import (
|
|
10
|
+
ClientEventHandler,
|
|
11
|
+
ServerPublicationContext,
|
|
12
|
+
ServerSubscribedContext,
|
|
13
|
+
ServerSubscribingContext,
|
|
14
|
+
ServerUnsubscribedContext,
|
|
15
|
+
ConnectedContext,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class RealtimeEventHandler(ClientEventHandler):
|
|
22
|
+
"""
|
|
23
|
+
Abstract base class for handling Realtime SDK events.
|
|
24
|
+
|
|
25
|
+
Subclass this and override `on_message` to handle incoming user updates.
|
|
26
|
+
The SDK handles channel parsing and data extraction for you.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
async def on_connected(self, ctx: ConnectedContext) -> None:
|
|
30
|
+
"""Called when connected to the Realtime Gateway."""
|
|
31
|
+
logger.info(f"✅ Connected to Realtime Gateway (Client ID: {ctx.client})")
|
|
32
|
+
|
|
33
|
+
async def on_subscribed(self, ctx: ServerSubscribedContext) -> None:
|
|
34
|
+
"""Called when server-side subscription is established."""
|
|
35
|
+
logger.info(f"✅ Server-side subscribed to channel: {ctx.channel}")
|
|
36
|
+
|
|
37
|
+
async def on_subscribing(self, ctx: ServerSubscribingContext) -> None:
|
|
38
|
+
"""Called when server-side subscription is in progress."""
|
|
39
|
+
logger.debug(f"Subscribing to server-side channel: {ctx.channel}")
|
|
40
|
+
|
|
41
|
+
async def on_unsubscribed(self, ctx: ServerUnsubscribedContext) -> None:
|
|
42
|
+
"""Called when unsubscribed from server-side subscription."""
|
|
43
|
+
logger.info(f"Unsubscribed from server-side channel: {ctx.channel}")
|
|
44
|
+
|
|
45
|
+
async def on_message(self, user_id: str, payload: Any) -> None:
|
|
46
|
+
"""
|
|
47
|
+
Called when a message is received for a specific user.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
user_id: The ID of the end-user this message is for.
|
|
51
|
+
payload: The message content (dict, string, etc).
|
|
52
|
+
"""
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
async def on_raw_message(self, channel: str, payload: Any) -> None:
|
|
56
|
+
"""
|
|
57
|
+
Called when a message is received but the user ID could not be parsed,
|
|
58
|
+
or for non-standard channels.
|
|
59
|
+
"""
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
async def on_publication(self, ctx: ServerPublicationContext) -> None:
|
|
63
|
+
"""
|
|
64
|
+
Internal handler. Parses channel context and dispatches to on_message.
|
|
65
|
+
"""
|
|
66
|
+
logger.info(f"📨 Publication received on channel: {ctx.channel}")
|
|
67
|
+
|
|
68
|
+
channel = self._extract_channel(ctx)
|
|
69
|
+
data = self._extract_data(ctx)
|
|
70
|
+
|
|
71
|
+
logger.debug(f"Channel: {channel}, Data: {data}")
|
|
72
|
+
|
|
73
|
+
user_id = self._extract_user_id(channel)
|
|
74
|
+
|
|
75
|
+
if user_id:
|
|
76
|
+
await self.on_message(user_id, data)
|
|
77
|
+
else:
|
|
78
|
+
await self.on_raw_message(channel, data)
|
|
79
|
+
|
|
80
|
+
def _extract_channel(self, ctx: ServerPublicationContext) -> str:
|
|
81
|
+
"""
|
|
82
|
+
Extract channel from context in a version-resilient way.
|
|
83
|
+
"""
|
|
84
|
+
# Try direct attribute
|
|
85
|
+
ch = getattr(ctx, "channel", None)
|
|
86
|
+
if ch:
|
|
87
|
+
return ch
|
|
88
|
+
|
|
89
|
+
# Try inside pub object
|
|
90
|
+
pub = getattr(ctx, "pub", None)
|
|
91
|
+
if pub:
|
|
92
|
+
return getattr(pub, "channel", "")
|
|
93
|
+
|
|
94
|
+
return ""
|
|
95
|
+
|
|
96
|
+
def _extract_data(self, ctx: ServerPublicationContext) -> Any:
|
|
97
|
+
"""
|
|
98
|
+
Extract data payload from context.
|
|
99
|
+
"""
|
|
100
|
+
pub = getattr(ctx, "pub", None)
|
|
101
|
+
return getattr(pub, "data", None) if pub else None
|
|
102
|
+
|
|
103
|
+
def _extract_user_id(self, channel: str) -> Optional[str]:
|
|
104
|
+
"""
|
|
105
|
+
Parse User ID from channel string.
|
|
106
|
+
Format confirmed as: personal:<target_user_id>#<service_user_id>
|
|
107
|
+
"""
|
|
108
|
+
if not channel:
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
# 1. Remove namespace (personal:)
|
|
112
|
+
if ":" in channel:
|
|
113
|
+
without_ns = channel.split(":", 1)[1]
|
|
114
|
+
else:
|
|
115
|
+
without_ns = channel
|
|
116
|
+
|
|
117
|
+
# 2. Extract first part before '#'
|
|
118
|
+
# "user_123#service_456" -> "user_123"
|
|
119
|
+
if "#" in without_ns:
|
|
120
|
+
return without_ns.split("#")[0]
|
|
121
|
+
|
|
122
|
+
return without_ns
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# Extending the SDK with Resource Clients
|
|
2
|
+
|
|
3
|
+
This directory contains the **base structure** for API resource clients.
|
|
4
|
+
|
|
5
|
+
## Recommended Pattern: Versioned Folders
|
|
6
|
+
|
|
7
|
+
When implementing resources, use **explicit versioned folders** that mirror bifrost's API structure:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
magick_mind/resources/
|
|
11
|
+
├── __init__.py # BaseResource, V1Resources, V2Resources
|
|
12
|
+
├── base.py # BaseResource class (or in __init__.py)
|
|
13
|
+
├── v1/
|
|
14
|
+
│ ├── __init__.py
|
|
15
|
+
│ ├── chat.py # ChatResourceV1
|
|
16
|
+
│ └── history.py # HistoryResourceV1
|
|
17
|
+
└── v2/
|
|
18
|
+
├── __init__.py
|
|
19
|
+
├── chat.py # ChatResourceV2
|
|
20
|
+
└── history.py # HistoryResourceV2
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
**Why versioned folders?** They mirror bifrost's API structure and keep versions isolated.
|
|
24
|
+
|
|
25
|
+
## Beta and Experimental Features
|
|
26
|
+
|
|
27
|
+
Beta features can be organized in two ways depending on how bifrost implements them:
|
|
28
|
+
|
|
29
|
+
### Pattern A: Beta as Separate Container
|
|
30
|
+
|
|
31
|
+
Use when beta is a **full API version** with breaking changes (preparation for v2):
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
resources/
|
|
35
|
+
├── v1/
|
|
36
|
+
│ └── chat.py # ChatResourceV1 → /v1/magickmind/chat
|
|
37
|
+
└── beta/
|
|
38
|
+
└── chat.py # ChatResourceBeta → /beta/magickmind/chat
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
class V1Resources:
|
|
43
|
+
def __init__(self, http_client):
|
|
44
|
+
self.chat = ChatResourceV1(http_client)
|
|
45
|
+
|
|
46
|
+
class BetaResources:
|
|
47
|
+
def __init__(self, http_client):
|
|
48
|
+
self.chat = ChatResourceBeta(http_client) # Breaking changes
|
|
49
|
+
|
|
50
|
+
# Usage
|
|
51
|
+
client.v1.chat.send(message="...") # Stable
|
|
52
|
+
client.beta.chat.send(content={...}) # Different API (breaking)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**When to use:**
|
|
56
|
+
- Beta has different request/response structure
|
|
57
|
+
- Testing major breaking changes
|
|
58
|
+
- Beta will become v2
|
|
59
|
+
|
|
60
|
+
### Pattern B: Beta as Attribute Within Version
|
|
61
|
+
|
|
62
|
+
Use when beta is a **feature flag** within the same version:
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
resources/
|
|
66
|
+
└── v1/
|
|
67
|
+
├── chat.py # ChatResourceV1 → /v1/magickmind/chat
|
|
68
|
+
└── chat_beta.py # ChatBetaResourceV1 → /v1/magickmind/chatbeta
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
class V1Resources:
|
|
73
|
+
def __init__(self, http_client):
|
|
74
|
+
self.chat = ChatResourceV1(http_client)
|
|
75
|
+
self.chat_beta = ChatBetaResourceV1(http_client) # Feature flag
|
|
76
|
+
|
|
77
|
+
# Usage
|
|
78
|
+
client.v1.chat.send(message="...") # Stable
|
|
79
|
+
client.v1.chat_beta.send( # Same structure, new features
|
|
80
|
+
message="...",
|
|
81
|
+
experimental_param=True # New parameter being tested
|
|
82
|
+
)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**When to use:**
|
|
86
|
+
- Beta has same basic structure as stable
|
|
87
|
+
- Testing new parameters/features
|
|
88
|
+
- A/B testing
|
|
89
|
+
- Beta is not a breaking change
|
|
90
|
+
|
|
91
|
+
### Pattern C: Both (Flexible)
|
|
92
|
+
|
|
93
|
+
You can use both patterns if bifrost has both types of beta features:
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
class V1Resources:
|
|
97
|
+
def __init__(self, http_client):
|
|
98
|
+
self.chat = ChatResourceV1(http_client)
|
|
99
|
+
self.chat_beta = ChatBetaResourceV1(http_client) # Feature flag
|
|
100
|
+
|
|
101
|
+
class BetaResources:
|
|
102
|
+
def __init__(self, http_client):
|
|
103
|
+
self.chat = ChatResourceBeta(http_client) # Breaking version
|
|
104
|
+
|
|
105
|
+
# Usage
|
|
106
|
+
client.v1.chat.send(...) # Stable v1
|
|
107
|
+
client.v1.chat_beta.send(...) # Beta feature in v1 (non-breaking)
|
|
108
|
+
client.beta.chat.send(...) # Beta API version (breaking)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
**Guideline:** Match the pattern to how bifrost actually implements the endpoint. Check the API path structure first.
|
|
112
|
+
|
|
113
|
+
## Example: V1 Chat Resource
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
# resources/v1/chat.py
|
|
117
|
+
from typing import Optional
|
|
118
|
+
from magick_mind.models.v1.chat import ChatSendRequest, ChatSendResponse
|
|
119
|
+
from magick_mind.resources.base import BaseResource
|
|
120
|
+
|
|
121
|
+
class ChatResourceV1(BaseResource):
|
|
122
|
+
def send(self, message: str, model: str = "gpt-4") -> ChatSendResponse:
|
|
123
|
+
"""Send a chat message."""
|
|
124
|
+
payload = ChatSendRequest(message=message, model=model)
|
|
125
|
+
|
|
126
|
+
# self.http gives access to authenticated httpx client
|
|
127
|
+
resp = self.http.post("/v1/chat/completions", json=payload.model_dump())
|
|
128
|
+
|
|
129
|
+
return ChatSendResponse(**resp.json())
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Resource Containers
|
|
133
|
+
|
|
134
|
+
Create version containers in `resources/__init__.py`:
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
from .v1.chat import ChatResourceV1
|
|
138
|
+
from .v1.history import HistoryResourceV1
|
|
139
|
+
|
|
140
|
+
class V1Resources:
|
|
141
|
+
"""Container for all v1 API resources."""
|
|
142
|
+
def __init__(self, http_client):
|
|
143
|
+
self.chat = ChatResourceV1(http_client)
|
|
144
|
+
self.history = HistoryResourceV1(http_client)
|
|
145
|
+
|
|
146
|
+
class V2Resources:
|
|
147
|
+
"""Container for all v2 API resources."""
|
|
148
|
+
def __init__(self, http_client):
|
|
149
|
+
from .v2.chat import ChatResourceV2
|
|
150
|
+
self.chat = ChatResourceV2(http_client)
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Wire to Main Client
|
|
154
|
+
|
|
155
|
+
Update `client.py`:
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
from magick_mind.resources import V1Resources, V2Resources
|
|
159
|
+
|
|
160
|
+
class MagickMind:
|
|
161
|
+
def __init__(self, ...):
|
|
162
|
+
# ... auth setup ...
|
|
163
|
+
|
|
164
|
+
# Initialize resources with http client
|
|
165
|
+
self.v1 = V1Resources(self.http)
|
|
166
|
+
self.v2 = V2Resources(self.http)
|
|
167
|
+
|
|
168
|
+
# Default alias
|
|
169
|
+
self.chat = self.v1.chat
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Usage
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
from magick_mind import MagickMind
|
|
176
|
+
|
|
177
|
+
client = MagickMind(email="...", password="...", base_url="...")
|
|
178
|
+
|
|
179
|
+
# Explicit version (recommended)
|
|
180
|
+
response = client.v1.chat.send(
|
|
181
|
+
api_key="sk-...",
|
|
182
|
+
message="Hello!",
|
|
183
|
+
chat_id="123",
|
|
184
|
+
sender_id="user-456"
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
# Or default alias
|
|
188
|
+
response = client.chat.send(...)
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## Complete Example
|
|
192
|
+
|
|
193
|
+
See **[docs/examples/chat_implementation/](../../../docs/examples/chat_implementation/)** for a complete working reference implementation.
|
|
194
|
+
|
|
195
|
+
## Benefits
|
|
196
|
+
|
|
197
|
+
1. **Explicit versioning** - `client.v1.chat` vs `client.v2.chat`
|
|
198
|
+
2. **Type safety** - Pydantic models validate requests/responses
|
|
199
|
+
3. **Mirrors Bifrost** - SDK structure matches API structure
|
|
200
|
+
4. **Safe evolution** - Breaking changes require explicit opt-in
|
|
201
|
+
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Resource clients for API endpoints."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from magick_mind.resources.base import BaseResource
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from magick_mind.http import HTTPClient
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class V1Resources:
|
|
12
|
+
"""Container for all v1 API resources."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, http_client: "HTTPClient"):
|
|
15
|
+
"""
|
|
16
|
+
Initialize V1 resources.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
http_client: HTTP client for making API requests
|
|
20
|
+
"""
|
|
21
|
+
from magick_mind.resources.v1.artifact import ArtifactResourceV1
|
|
22
|
+
from magick_mind.resources.v1.api_keys import ApiKeysResourceV1
|
|
23
|
+
from magick_mind.resources.v1.chat import ChatResourceV1
|
|
24
|
+
from magick_mind.resources.v1.corpus import CorpusResourceV1
|
|
25
|
+
from magick_mind.resources.v1.end_user import EndUserResourceV1
|
|
26
|
+
from magick_mind.resources.v1.history import HistoryResourceV1
|
|
27
|
+
from magick_mind.resources.v1.mindspace import MindspaceResourceV1
|
|
28
|
+
from magick_mind.resources.v1.project import ProjectResourceV1
|
|
29
|
+
from magick_mind.resources.v1.model import ModelsResourceV1
|
|
30
|
+
|
|
31
|
+
self.artifact = ArtifactResourceV1(http_client)
|
|
32
|
+
self.api_keys = ApiKeysResourceV1(http_client)
|
|
33
|
+
self.chat = ChatResourceV1(http_client)
|
|
34
|
+
self.corpus = CorpusResourceV1(http_client)
|
|
35
|
+
self.end_user = EndUserResourceV1(http_client)
|
|
36
|
+
self.history = HistoryResourceV1(http_client)
|
|
37
|
+
self.mindspace = MindspaceResourceV1(http_client)
|
|
38
|
+
self.project = ProjectResourceV1(http_client)
|
|
39
|
+
self.models = ModelsResourceV1(http_client)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
__all__ = ["BaseResource", "V1Resources"]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Base class for API resource clients."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from magick_mind.http import HTTPClient
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BaseResource:
|
|
10
|
+
"""
|
|
11
|
+
Base class for all resource clients.
|
|
12
|
+
|
|
13
|
+
Resource clients encapsulate API endpoints for specific domains
|
|
14
|
+
(e.g., chat, history, users, mindspaces).
|
|
15
|
+
|
|
16
|
+
For a complete implementation example, see docs/contributing/resource_implementation_guide/
|
|
17
|
+
|
|
18
|
+
Example:
|
|
19
|
+
class ChatResourceV1(BaseResource):
|
|
20
|
+
def send(self, api_key: str, message: str, **kwargs):
|
|
21
|
+
return self._http.post("/v1/magickmind/chat", json={...})
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, http_client: "HTTPClient"):
|
|
25
|
+
"""
|
|
26
|
+
Initialize resource client.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
http_client: HTTP client for making API requests
|
|
30
|
+
"""
|
|
31
|
+
self._http = http_client
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""V1 API resources."""
|
|
2
|
+
|
|
3
|
+
from magick_mind.resources.v1.artifact import ArtifactResourceV1
|
|
4
|
+
from magick_mind.resources.v1.api_keys import ApiKeysResourceV1
|
|
5
|
+
from magick_mind.resources.v1.corpus import CorpusResourceV1
|
|
6
|
+
from magick_mind.resources.v1.chat import ChatResourceV1
|
|
7
|
+
from magick_mind.resources.v1.end_user import EndUserResourceV1
|
|
8
|
+
from magick_mind.resources.v1.mindspace import MindspaceResourceV1
|
|
9
|
+
from magick_mind.resources.v1.project import ProjectResourceV1
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"ArtifactResourceV1",
|
|
13
|
+
"ApiKeysResourceV1",
|
|
14
|
+
"CorpusResourceV1",
|
|
15
|
+
"ChatResourceV1",
|
|
16
|
+
"EndUserResourceV1",
|
|
17
|
+
"MindspaceResourceV1",
|
|
18
|
+
"ProjectResourceV1",
|
|
19
|
+
]
|