Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion sdks/python/src/opik/api_objects/experiment/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ def build_metadata_and_prompt_versions(
experiment_config["prompts"] = {}

for prompt_obj in prompts:
prompt_versions.append({"id": prompt_obj.__internal_api__version_id__})
if prompt_obj.__internal_api__version_id__ is not None:
prompt_versions.append({"id": prompt_obj.__internal_api__version_id__})
# Use __internal_api__to_info_dict__() to get the prompt content in a consistent way
prompt_info = prompt_obj.__internal_api__to_info_dict__()
# Extract the template/messages from the version dict
Expand Down
6 changes: 3 additions & 3 deletions sdks/python/src/opik/api_objects/prompt/base_prompt.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def commit(self) -> Optional[str]:

@property
@abstractmethod
def version_id(self) -> str:
def version_id(self) -> Optional[str]:
"""The unique identifier of the prompt version."""
pass

Expand Down Expand Up @@ -79,8 +79,8 @@ def project_name(self) -> Optional[str]:
pass

# Internal API fields for backend synchronization
__internal_api__prompt_id__: str
__internal_api__version_id__: str
__internal_api__prompt_id__: Optional[str]
__internal_api__version_id__: Optional[str]

@abstractmethod
def format(self, *args: Any, **kwargs: Any) -> Any:
Expand Down
98 changes: 66 additions & 32 deletions sdks/python/src/opik/api_objects/prompt/chat/chat_prompt.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import copy
import json
import logging
from typing import Any, Dict, List, Optional, Tuple, Type

import httpx

from typing_extensions import override

from opik.rest_api import core as rest_api_core
from opik.rest_api import types as rest_api_types
from opik.validation import chat_prompt_messages, validator

Expand All @@ -12,6 +16,8 @@
from .. import types as prompt_types
from . import chat_prompt_template

LOGGER = logging.getLogger(__name__)


class ChatPrompt(base_prompt.BasePrompt):
"""
Expand Down Expand Up @@ -75,10 +81,11 @@ def __init__(
self._tags = copy.copy(tags) if tags else []
self._project_name = project_name
self._commit: Optional[str] = None
self.__internal_api__prompt_id__: str
self.__internal_api__version_id__: str
self.__internal_api__prompt_id__: Optional[str] = None
self.__internal_api__version_id__: Optional[str] = None
self._synced: bool = False

self._sync_with_backend()
self.sync_with_backend()
Comment thread
petrotiurin marked this conversation as resolved.

def _validate_inputs(self, **kwargs: Any) -> None:
for parameter, validator_class in self._parameter_validators:
Expand All @@ -87,35 +94,61 @@ def _validate_inputs(self, **kwargs: Any) -> None:
validator_instance.validate()
validator_instance.raise_if_validation_failed()

def _sync_with_backend(self) -> None:
from opik.api_objects import opik_client

opik_client_ = opik_client.get_global_client()
prompt_client_ = prompt_client.PromptClient(opik_client_.rest_client)

# Convert messages array to JSON string for backend storage
messages_str = json.dumps(self._messages)

prompt_version = prompt_client_.create_prompt(
name=self._name,
prompt=messages_str,
metadata=self._metadata,
type=self._type,
template_structure="chat",
id=self._id,
description=self._description,
change_description=self._change_description,
tags=self._tags,
project_name=self._project_name,
)
@property
def synced(self) -> bool:
"""Whether the chat prompt has been successfully synced with the backend."""
return self._synced

self._commit = prompt_version.commit
self.__internal_api__prompt_id__ = prompt_version.prompt_id
self.__internal_api__version_id__ = prompt_version.id
# Update fields from backend response to ensure consistency
self._id = prompt_version.id
self._change_description = prompt_version.change_description
self._tags = prompt_version.tags
def sync_with_backend(self) -> bool:
"""Synchronize the chat prompt with the backend.

Creates or updates the chat prompt on the Opik server. If the sync fails,
a warning is logged and the prompt continues to work locally.

Returns:
True if the sync succeeded, False otherwise.
"""
try:
from opik.api_objects import opik_client

Comment thread
petrotiurin marked this conversation as resolved.
opik_client_ = opik_client.get_global_client()
prompt_client_ = prompt_client.PromptClient(opik_client_.rest_client)

# Convert messages array to JSON string for backend storage
messages_str = json.dumps(self._messages)

prompt_version = prompt_client_.create_prompt(
name=self._name,
prompt=messages_str,
metadata=self._metadata,
type=self._type,
template_structure="chat",
id=self._id,
description=self._description,
change_description=self._change_description,
tags=self._tags,
project_name=self._project_name,
)

self._commit = prompt_version.commit
self.__internal_api__prompt_id__ = prompt_version.prompt_id
self.__internal_api__version_id__ = prompt_version.id
# Update fields from backend response to ensure consistency
self._id = prompt_version.id
self._change_description = prompt_version.change_description
self._tags = prompt_version.tags
self._synced = True
return True
except (rest_api_core.ApiError, httpx.ConnectError, httpx.TimeoutException):
LOGGER.warning(
"Failed to sync chat prompt '%s' with the backend. "
"The prompt will work locally but is not persisted on the server. "
"You can retry by calling .sync_with_backend().",
self._name,
exc_info=True,
Comment thread
petrotiurin marked this conversation as resolved.
)
self._synced = False
return False

@property
@override
Expand All @@ -136,7 +169,7 @@ def commit(self) -> Optional[str]:

@property
@override
def version_id(self) -> str:
def version_id(self) -> Optional[str]:
"""The unique identifier of the prompt version."""
return self.__internal_api__version_id__

Expand Down Expand Up @@ -278,4 +311,5 @@ def from_fern_prompt_version(
copy.copy(prompt_version.tags) if prompt_version.tags else []
)
chat_prompt._project_name = project_name
chat_prompt._synced = True
return chat_prompt
87 changes: 61 additions & 26 deletions sdks/python/src/opik/api_objects/prompt/text/prompt.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
import json
import logging
from typing import Any, Dict, Optional, Union, List

import httpx
from typing_extensions import override
from opik.rest_api import core as rest_api_core
from opik.rest_api import types as rest_api_types
from . import prompt_template
from .. import types as prompt_types
Expand Down Expand Up @@ -62,32 +65,63 @@ def __init__(
self._tags = copy.copy(tags) if tags else []
self._project_name = project_name

self._sync_with_backend()

def _sync_with_backend(self) -> None:
from opik.api_objects import opik_client

opik_client_ = opik_client.get_global_client()
prompt_client_ = prompt_client.PromptClient(opik_client_.rest_client)
prompt_version = prompt_client_.create_prompt(
name=self._name,
prompt=self._template.text,
metadata=self._metadata,
type=self._type,
id=self._id,
description=self._description,
change_description=self._change_description,
tags=self._tags,
project_name=self._project_name,
)
self._commit: Optional[str] = None
self.__internal_api__prompt_id__: Optional[str] = None
self.__internal_api__version_id__: Optional[str] = None
self._synced: bool = False

self.sync_with_backend()

@property
def synced(self) -> bool:
Comment thread
petrotiurin marked this conversation as resolved.
"""Whether the prompt has been successfully synced with the backend."""
return self._synced

self._commit = prompt_version.commit
self.__internal_api__prompt_id__ = prompt_version.prompt_id
self.__internal_api__version_id__ = prompt_version.id
# Update fields from backend response to ensure consistency
self._id = prompt_version.id
self._change_description = prompt_version.change_description
self._tags = prompt_version.tags
def sync_with_backend(self) -> bool:
"""Synchronize the prompt with the backend.

Creates or updates the prompt on the Opik server. If the sync fails,
a warning is logged and the prompt continues to work locally.

Returns:
True if the sync succeeded, False otherwise.
"""
try:
from opik.api_objects import opik_client

opik_client_ = opik_client.get_global_client()
prompt_client_ = prompt_client.PromptClient(opik_client_.rest_client)
prompt_version = prompt_client_.create_prompt(
name=self._name,
prompt=self._template.text,
metadata=self._metadata,
type=self._type,
id=self._id,
description=self._description,
change_description=self._change_description,
tags=self._tags,
project_name=self._project_name,
)

self._commit = prompt_version.commit
self.__internal_api__prompt_id__ = prompt_version.prompt_id
self.__internal_api__version_id__ = prompt_version.id
# Update fields from backend response to ensure consistency
self._id = prompt_version.id
self._change_description = prompt_version.change_description
self._tags = prompt_version.tags
self._synced = True
return True
except (rest_api_core.ApiError, httpx.ConnectError, httpx.TimeoutException):
LOGGER.warning(
"Failed to sync prompt '%s' with the backend. "
"The prompt will work locally but is not persisted on the server. "
"You can retry by calling .sync_with_backend().",
self._name,
exc_info=True,
)
self._synced = False
return False

@property
@override
Expand All @@ -108,7 +142,7 @@ def commit(self) -> Optional[str]:

@property
@override
def version_id(self) -> str:
def version_id(self) -> Optional[str]:
"""The unique identifier of the prompt version."""
return self.__internal_api__version_id__

Expand Down Expand Up @@ -242,4 +276,5 @@ def from_fern_prompt_version(
prompt._change_description = prompt_version.change_description
prompt._tags = copy.copy(prompt_version.tags) if prompt_version.tags else []
prompt._project_name = project_name
prompt._synced = True
return prompt
Loading
Loading