1010
1111from __future__ import annotations
1212
13+ import json as json_lib
1314import logging
1415from typing import Any
1516
@@ -54,6 +55,8 @@ def _request(
5455 * ,
5556 params : dict [str , Any ] | None = None ,
5657 json : Any = None ,
58+ files : dict [str , Any ] | None = None ,
59+ data : dict [str , Any ] | None = None ,
5760 ) -> Any :
5861 url = self ._url (path )
5962 # Redact secrets from logs: only entity path + method, never body.
@@ -63,6 +66,8 @@ def _request(
6366 url ,
6467 params = params ,
6568 json = json ,
69+ files = files ,
70+ data = data ,
6671 timeout = self .timeout ,
6772 verify = self .verify ,
6873 )
@@ -164,67 +169,104 @@ def list_custom_tools(self) -> list[dict[str, Any]]:
164169 result = self ._request ("GET" , "prompt-studio/" )
165170 return result if isinstance (result , list ) else result .get ("results" , [])
166171
167- def get_custom_tool (self , tool_id : str ) -> dict [str , Any ]:
168- """Tool detail; response includes embedded ``prompts`` + ``default_profile``."""
169- return self ._request ("GET" , f"prompt-studio/{ tool_id } /" )
170-
171- def create_custom_tool (self , payload : dict [str , Any ]) -> dict [str , Any ]:
172- """Create a custom tool. Backend also auto-creates one default ProfileManager."""
173- return self ._request ("POST" , "prompt-studio/" , json = payload )
174-
175- def export_custom_tool (self , tool_id : str , * , force : bool = True ) -> Any :
176- """Republish ``PromptStudioRegistry`` from the tool's current target state.
172+ def list_profiles (self , tool_id : str ) -> list [dict [str , Any ]]:
173+ """List ProfileManager rows for a tool.
177174
178- Used after profile+prompt reconciliation so the registry row is
179- rebuilt without the SDK ever carrying ``tool_metadata`` across orgs.
175+ Migration reads this on the source only — to discover the
176+ default profile's adapter UUIDs so they can be remapped to
177+ target adapter ids for ``import_project``.
180178 """
181- return self ._request (
182- "POST" ,
183- f"prompt-studio/export/{ tool_id } " ,
184- json = {
185- "is_shared_with_org" : False ,
186- "user_id" : [],
187- "force_export" : force ,
188- },
189- )
190-
191- # ----- profile managers -----
192-
193- def list_profiles (self , tool_id : str ) -> list [dict [str , Any ]]:
194- """List ProfileManager rows for a tool via the per-tool list action."""
195179 result = self ._request (
196180 "GET" , f"prompt-studio/prompt-studio-profile/{ tool_id } /"
197181 )
198182 return result if isinstance (result , list ) else result .get ("results" , [])
199183
200- def create_profile (self , tool_id : str , payload : dict [str , Any ]) -> dict [str , Any ]:
201- """POST to ``prompt-studio/profilemanager/{tool_id}`` (no trailing slash)."""
184+ def export_project (self , tool_id : str ) -> dict [str , Any ]:
185+ """Export a prompt-studio project as a portable JSON blob.
186+
187+ Bundles ``tool_metadata``, ``tool_settings``,
188+ ``default_profile_settings``, ``prompts``, ``export_metadata`` in
189+ one shot — feed straight into ``import_project`` or
190+ ``sync_prompts`` on the target.
191+ """
192+ return self ._request ("GET" , f"prompt-studio/project-transfer/{ tool_id } " )
193+
194+ def import_project (
195+ self ,
196+ export_data : dict [str , Any ],
197+ adapter_ids : dict [str , str | None ] | None = None ,
198+ ) -> dict [str , Any ]:
199+ """Import a prompt-studio project from an export blob.
200+
201+ Backend creates the tool, builds the default ProfileManager from
202+ the supplied target-org adapter ids, and imports all prompts in
203+ one call. On name collision the backend silently uniquifies the
204+ new tool's name — callers should pre-check via
205+ ``list_custom_tools`` to avoid that.
206+
207+ ``adapter_ids`` keys are the backend's form fields:
208+ ``llm_adapter_id``, ``vector_db_adapter_id``,
209+ ``embedding_adapter_id``, ``x2text_adapter_id``. All four
210+ required to wire the profile; otherwise backend falls back to
211+ a profile without adapters and flags ``needs_adapter_config``.
212+ """
213+ tool_name = (
214+ export_data .get ("tool_metadata" , {}).get ("tool_name" ) or "export"
215+ )
216+ content = json_lib .dumps (export_data ).encode ()
217+ files = {"file" : (f"{ tool_name } .json" , content , "application/json" )}
218+ data : dict [str , Any ] = {}
219+ if adapter_ids :
220+ for key in (
221+ "llm_adapter_id" ,
222+ "vector_db_adapter_id" ,
223+ "embedding_adapter_id" ,
224+ "x2text_adapter_id" ,
225+ ):
226+ val = adapter_ids .get (key )
227+ if val :
228+ data [key ] = val
202229 return self ._request (
203- "POST" , f"prompt-studio/profilemanager/{ tool_id } " , json = payload
230+ "POST" ,
231+ "prompt-studio/project-transfer/" ,
232+ files = files ,
233+ data = data ,
204234 )
205235
206- def delete_profile (self , profile_id : str ) -> None :
207- self ._request ("DELETE" , f"profile-manager/{ profile_id } /" )
236+ def sync_prompts (
237+ self ,
238+ tool_id : str ,
239+ export_data : dict [str , Any ],
240+ * ,
241+ create_copy : bool = False ,
242+ ) -> dict [str , Any ]:
243+ """Rip-and-replace prompts on an existing target tool.
208244
209- def set_default_profile (self , tool_id : str , profile_id : str ) -> Any :
210- """Mark a single profile as default for this tool (zeros the rest)."""
245+ Adopt path: target tool already exists with its own
246+ adapter-bound profiles. This overwrites its prompt set (and
247+ ``tool_settings``) from source; profiles and uploaded documents
248+ are left untouched.
249+ """
250+ payload = {"data" : export_data , "create_copy" : create_copy }
211251 return self ._request (
212- "PATCH" ,
213- f"prompt-studio/prompt-studio-profile/{ tool_id } /" ,
214- json = {"default_profile" : profile_id },
252+ "POST" , f"prompt-studio/{ tool_id } /sync-prompts/" , json = payload
215253 )
216254
217- # ----- prompts -----
218-
219- def list_prompts (self , * , tool_id : str ) -> list [dict [str , Any ]]:
220- """List prompts filtered by tool_id (FilterHelper-backed)."""
221- result = self ._request ("GET" , "prompt/" , params = {"tool_id" : tool_id })
222- return result if isinstance (result , list ) else result .get ("results" , [])
255+ def export_custom_tool (self , tool_id : str , * , force : bool = True ) -> Any :
256+ """Republish ``PromptStudioRegistry`` from the tool's current state.
223257
224- def create_prompt (self , tool_id : str , payload : dict [str , Any ]) -> dict [str , Any ]:
225- """POST to ``prompt-studio/prompt-studio-prompt/{tool_id}/`` (create_prompt action)."""
258+ Called after import/sync so the registry row reflects the
259+ freshly landed prompts. Required for ToolInstancePhase to find
260+ a target registry id to remap.
261+ """
226262 return self ._request (
227- "POST" , f"prompt-studio/prompt-studio-prompt/{ tool_id } /" , json = payload
263+ "POST" ,
264+ f"prompt-studio/export/{ tool_id } " ,
265+ json = {
266+ "is_shared_with_org" : False ,
267+ "user_id" : [],
268+ "force_export" : force ,
269+ },
228270 )
229271
230272 # ----- workflows -----
0 commit comments